/* * Copyright (c) 2008 by Michael Kiefte * * 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.tools.imports.adc2; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.font.TextAttribute; import java.awt.geom.GeneralPath; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import javax.imageio.ImageIO; import org.apache.commons.io.FileUtils; import VASSAL.Info; import VASSAL.build.AbstractConfigurable; import VASSAL.build.GameModule; import VASSAL.build.module.GlobalOptions; import VASSAL.build.module.Inventory; import VASSAL.build.module.Map; import VASSAL.build.module.PrototypeDefinition; import VASSAL.build.module.PrototypesContainer; import VASSAL.build.module.ToolbarMenu; import VASSAL.build.module.map.BoardPicker; import VASSAL.build.module.map.LayerControl; import VASSAL.build.module.map.LayeredPieceCollection; import VASSAL.build.module.map.SetupStack; import VASSAL.build.module.map.Zoomer; import VASSAL.build.module.map.boardPicker.Board; import VASSAL.build.module.map.boardPicker.board.HexGrid; import VASSAL.build.module.map.boardPicker.board.MapGrid; import VASSAL.build.module.map.boardPicker.board.MapGrid.BadCoords; import VASSAL.build.module.map.boardPicker.board.SquareGrid; import VASSAL.build.module.map.boardPicker.board.ZonedGrid; import VASSAL.build.module.map.boardPicker.board.mapgrid.HexGridNumbering; import VASSAL.build.module.map.boardPicker.board.mapgrid.RegularGridNumbering; import VASSAL.build.module.map.boardPicker.board.mapgrid.SquareGridNumbering; import VASSAL.build.module.map.boardPicker.board.mapgrid.Zone; import VASSAL.build.widget.PieceSlot; import VASSAL.configure.StringArrayConfigurer; import VASSAL.counters.BasicPiece; import VASSAL.counters.GamePiece; import VASSAL.counters.Immobilized; import VASSAL.counters.Marker; import VASSAL.counters.UsePrototype; import VASSAL.tools.SequenceEncoder; import VASSAL.tools.filechooser.ExtensionFileFilter; import VASSAL.tools.imports.FileFormatException; import VASSAL.tools.imports.Importer; import VASSAL.tools.io.IOUtils; /** * The map board itself. * * @author Michael Kiefte * */ public class MapBoard extends Importer { private static final String PLACE_NAME = "Location Names"; protected class MapLayer { private final ArrayList<? extends MapDrawable> elements; private final String name; private final boolean switchable; protected ArrayList<MapLayer> layers = null; protected String imageName; boolean shouldDraw = true; MapLayer(ArrayList<? extends MapDrawable> elements, String name, boolean switchable) { this.elements = elements; this.name = name; this.switchable = switchable; } void writeToArchive() throws IOException { // write piece Rectangle r = writeImageToArchive(); if (imageName != null && r != null && r.width > 0 && r.height > 0) { SequenceEncoder se = new SequenceEncoder(';'); se.append("").append("").append(imageName).append(getName()); GamePiece gp = new BasicPiece(BasicPiece.ID + se.getValue()); gp = new Marker(Marker.ID + "Layer", gp); gp.setProperty("Layer", getName()); gp = new Marker(Marker.ID + "Type", gp); gp.setProperty("Type", "Layer"); gp = new Immobilized(gp, Immobilized.ID + "n;V"); // create layer LayeredPieceCollection l = getLayeredPieceCollection(); String order = l.getAttributeValueString(LayeredPieceCollection.LAYER_ORDER); if (order.equals("")) { order = getName(); } else { order = order + "," + getName(); } l.setAttribute(LayeredPieceCollection.LAYER_ORDER, order); Map mainMap = getMainMap(); Board board = getBoard(); SetupStack stack = new SetupStack(); insertComponent(stack, mainMap); Point p = new Point(r.x + r.width/2, r.y + r.height/2); stack.setAttribute(SetupStack.NAME, getName()); stack.setAttribute(SetupStack.OWNING_BOARD, board.getConfigureName()); stack.setAttribute(SetupStack.X_POSITION, Integer.toString(p.x)); stack.setAttribute(SetupStack.Y_POSITION, Integer.toString(p.y)); PieceSlot slot = new PieceSlot(gp); insertComponent(slot, stack); if (isSwitchable()) { // TODO: initial state of layer visibility // add stack layer control LayerControl control = new LayerControl(); insertComponent(control, l); control.setAttribute(LayerControl.BUTTON_TEXT, getName()); control.setAttribute(LayerControl.TOOLTIP, "Toggle " + getName().toLowerCase() + " visibility"); control.setAttribute(LayerControl.COMMAND, LayerControl.CMD_TOGGLE); control.setAttribute(LayerControl.LAYERS, getName()); // one toolbar menu to control all mapboard elements. ToolbarMenu menu = getToolbarMenu(); String entries = menu.getAttributeValueString(ToolbarMenu.MENU_ITEMS); if (entries.equals("")) { entries = getName(); } else { entries = entries + "," + new SequenceEncoder(getName(), ',').getValue(); } menu.setAttribute(ToolbarMenu.MENU_ITEMS, entries); } } } /** * @throws IOException */ protected Rectangle writeImageToArchive() throws IOException { // write image to archive final BufferedImage image = getLayerImage(); if (image == null) { return null; } final Rectangle r = getCropRectangle(image); if (r.width == 0 || r.height == 0) { return null; } final File f = File.createTempFile("map", ".png", Info.getTempDir()); try { ImageIO.write(image.getSubimage(r.x, r.y, r.width, r.height), "png", f); imageName = getUniqueImageFileName(getName(), ".png"); GameModule.getGameModule() .getArchiveWriter() .addImage(f.getPath(), imageName); return r; } finally { FileUtils.forceDelete(f); } } protected Rectangle getCropRectangle(BufferedImage image) { Rectangle r = new Rectangle(getLayout().getBoardSize()); leftside: while (true) { for (int i = r.y; i < r.y + r.height; ++i) { if (image.getRGB(r.x, i) != 0) { break leftside; } } ++r.x; --r.width; if (r.width == 0) { r.height = 0; return r; } } topside: while (true) { for (int i = r.x; i < r.x + r.width; ++i) { if (image.getRGB(i, r.y) != 0) { break topside; } } ++r.y; --r.height; } rightside: while (true) { for (int i = r.y; i < r.y + r.height; ++i) { if (image.getRGB(r.x + r.width - 1, i) != 0) { break rightside; } } --r.width; } bottomside: while (true) { for (int i = r.x; i < r.x + r.width; ++i) { if (image.getRGB(i, r.y + r.height - 1) != 0) { break bottomside; } } --r.height; } return r; } void overlay(MapLayer layer) { if (layers == null) { layers = new ArrayList<MapLayer>(); } layers.add(layer); } protected AlphaComposite getComposite() { return AlphaComposite.SrcAtop; } BufferedImage getLayerImage() { Dimension d = getLayout().getBoardSize(); BufferedImage image = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = image.createGraphics(); if (draw(g)) { if (layers != null) { g.setComposite(getComposite()); for (MapLayer l : layers) { l.draw(g); } } } else { image = null; } return image; } boolean draw(Graphics2D g) { if (shouldDraw) { shouldDraw = false; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); for (MapDrawable m : elements) { if (m.draw(g)) { shouldDraw = true; } } } return shouldDraw; } boolean isSwitchable() { return switchable; } String getName() { return name; } boolean hasElements() { return !elements.isEmpty(); } } class BaseLayer extends MapLayer { BaseLayer() { super(null, "Base Layer", false); } boolean hasBaseMap() { File underlay = action.getCaseInsensitiveFile(new File(forceExtension(path, "sml")), null, false, null); if (underlay == null) { underlay = action.getCaseInsensitiveFile(new File(stripExtension(path) + "-Z" + (zoomLevel+1) + ".bmp"), null, false, null); } return underlay != null; } @Override boolean draw(Graphics2D g) { // set background color g.setBackground(tableColor); Dimension d = getLayout().getBoardSize(); g.clearRect(0, 0, d.width, d.height); // See if map image file exists File sml = action.getCaseInsensitiveFile(new File(forceExtension(path, "sml")), null, false, null); if (sml != null) { try { readScannedMapLayoutFile(sml, g); } // FIXME: review error message catch (IOException e) {} } else if (getSet().underlay != null) { // If sml file doesn't exist, see if there is a single-sheet underlay image g.drawImage(getSet().underlay, null, 0, 0); } return true; } @Override void writeToArchive() throws IOException { // write the underlay map image writeImageToArchive(); assert(imageName != null); Board board = getBoard(); board.setAttribute(Board.IMAGE, imageName); board.setConfigureName(baseName); // so we can get hex labels getMainMap().setBoards(Collections.singleton(board)); } @Override protected Rectangle getCropRectangle(BufferedImage image) { return new Rectangle(getLayout().getBoardSize()); } @Override protected AlphaComposite getComposite() { return AlphaComposite.SrcOver; } } /** * A layout consisting of squares in a checkerboard pattern (<it>i.e.</it> each * square has four neighbours). * * @author Michael Kiefte * */ protected class GridLayout extends Layout { GridLayout(int size, int columns, int rows) { super(size, columns, rows); } @Override Point coordinatesToPosition(int x, int y, boolean nullIfOffBoard) { if (!nullIfOffBoard || isOnMapBoard(x, y)) { int xx = getDeltaX() * x; int yy = getDeltaY() * y; return new Point(xx, yy); } else return null; } @Override Dimension getBoardSize() { Dimension d = new Dimension(); d.width = getDeltaX() * nColumns; d.height = getDeltaY() * nRows; return d; } @Override int getDeltaX() { return getHexSize(); } @Override int getDeltaY() { return getHexSize(); } @Override Point getOrigin() { return new Point(getHexSize() / 2, getHexSize() / 2); } @Override SquareGrid getGeometricGrid() { SquareGrid grid = new SquareGrid(); grid.setOrigin(getOrigin()); grid.setDx(getDeltaX()); grid.setDy(getDeltaY()); return grid; } @Override Rectangle getRectangle(MapSheet map) { Rectangle r = map.getField(); Point upperLeft = coordinatesToPosition(r.x, r.y, false); Point lowerRight = coordinatesToPosition(r.x + r.width - 1, r.y + r.height - 1, false); // get lower right-hand corner of lower right-hand square lowerRight.x += getHexSize() - 1; lowerRight.y += getHexSize() - 1; constrainRectangle(upperLeft, lowerRight); return new Rectangle(upperLeft.x, upperLeft.y, lowerRight.x - upperLeft.x + 1, lowerRight.y - upperLeft.y + 1); } @Override RegularGridNumbering getGridNumbering() { return new SquareGridNumbering(); } @Override int getNFaces() { return 4; } } /** * Redundant information about each hex. So far only used for determining * the default order of line definitions for hex sides and hex lines. */ private static class Hex { ArrayList<Line> hexLines = new ArrayList<Line>(); ArrayList<Line> hexSides = new ArrayList<Line>(); } /** * Mapboard element based on terrain symbol from <code>SymbolSet</code>. Not necessarily hexagonal but can also be square. */ protected class HexData extends MapDrawable { final SymbolSet.SymbolData symbol; HexData(int index, SymbolSet.SymbolData symbol) { super(index); assert (symbol != null); this.symbol = symbol; } @Override boolean draw(Graphics2D g) { Point p = getPosition(); if (symbol != null && !symbol.isTransparent()) { g.drawImage(symbol.getImage(), null, p.x, p.y); return true; } else { return false; } } } /** * Symbol that is placed in every hex. */ protected class MapBoardOverlay extends HexData { @Override boolean draw(Graphics2D g) { if (symbol != null) { for (int y = 0; y < getNRows(); ++y) { for (int x = 0; x < getNColumns(); ++x) { Point p = coordinatesToPosition(x, y); g.drawImage(symbol.getImage(), null, p.x, p.y); } } return true; } else { return false; } } // doesn't have an index MapBoardOverlay(SymbolSet.SymbolData symbol) { super(-1, symbol); } } /** * A line from a hex edge to the centre as in the spoke of a wheel. Typically used for terrain * features such as roads etc. */ protected class HexLine extends Line { private final int direction; HexLine(int index, int line, int direction) { super(index, line); this.direction = direction; } @Override int compare(LineDefinition o1, LineDefinition o2) { if (o1 == null && o2 == null) return 0; else if (o1 == null) return 1; else if (o2 == null) return -1; int priority = o1.getHexLineDrawPriority() - o2.getHexLineDrawPriority(); if (priority != 0) return priority; else return super.compare(o1, o2); } /* * Under normal circumstances, map board elements get drawn one at a time. Hex sides and hex lines are * actually continuous from one hex or square to the next. Although ADC2 draws each segment separately * anyway, this creates poor-looking corner and edge effects. Instead of that, the importer composes longer * lines made up of many continuous smaller segments and then draws a single line in one go. To do this, * instead of drawing itself as it's called, each segment adds itself to a line and the last segment in the * list calls the draw method for all of the lines thus created. */ @Override boolean draw(Graphics2D g) { LineDefinition l = getLine(); boolean result = false; if (l != null) { result = true; Point pos = getPosition(); int size = getLayout().getHexSize(); pos.translate(size / 2, size / 2); Layout lo = getLayout(); // if ((direction & 0x1) > 0) // horizontal north west if ((direction & 0x6) > 0) {// north west; 0x4 = version 1 Point nw = lo.getNorthWest(hexIndex); nw.translate(size / 2, size / 2); l.addLine(pos.x, pos.y, (pos.x + nw.x) / 2.0f, (pos.y + nw.y) / 2.0f); } if ((direction & 0x8) > 0) {// west Point w = lo.getWest(hexIndex); w.translate(size / 2, size / 2); l.addLine(pos.x, pos.y, (pos.x + w.x) / 2.0f, (pos.y + w.y) / 2.0f); } if ((direction & 0x30) > 0) { // south west; 0x10 = version 1 Point sw = lo.getSouthWest(hexIndex); sw.translate(size / 2, size / 2); l.addLine(pos.x, pos.y, (pos.x + sw.x) / 2.0f, (pos.y + sw.y) / 2.0f); } // if ((direction & 0x40) > 0) // horizontal south west if ((direction & 0x80) > 0) {// south Point s = lo.getSouth(hexIndex); s.translate(size / 2, size / 2); l.addLine(pos.x, pos.y, (pos.x + s.x) / 2.0f, (pos.y + s.y) / 2.0f); } if ((direction & 0x100) > 0) {// north Point n = lo.getNorth(hexIndex); n.translate(size / 2, size / 2); l.addLine(pos.x, pos.y, (pos.x + n.x) / 2.0f, (pos.y + n.y) / 2.0f); } // if ((direction & 0x200) > 0) // horizontal north east if ((direction & 0xC00) > 0) { // north east; 0x800 = version 1 Point ne = lo.getNorthEast(hexIndex); ne.translate(size / 2, size / 2); l.addLine(pos.x, pos.y, (pos.x + ne.x) / 2.0f, (pos.y + ne.y) / 2.0f); } if ((direction & 0x1000) > 0) {// east Point e = lo.getEast(hexIndex); e.translate(size / 2, size / 2); l.addLine(pos.x, pos.y, (pos.x + e.x) / 2.0f, (pos.y + e.y) / 2.0f); } if ((direction & 0x6000) > 0) { // south east; 0x2000 = version 1 Point se = lo.getSouthEast(hexIndex); se.translate(size / 2, size / 2); l.addLine(pos.x, pos.y, (pos.x + se.x) / 2.0f, (pos.y + se.y) / 2.0f); } // if ((direction & 0x8000) > 0) // horizontal south east } // if this is the last one, draw all of the compiled lines. if (this == hexLines.get(hexLines.size() - 1)) { drawLines(g, BasicStroke.CAP_BUTT); } return result; } @Override ArrayList<Line> getLineList(Hex h) { return h.hexLines; } } /** * The edges of a hex or square. */ protected class HexSide extends Line { // flags indicating which side to draw. private final int side; HexSide(int index, int line, int side) { super(index, line); this.side = side; } @Override int compare(LineDefinition o1, LineDefinition o2) { if (o1 == null && o2 == null) return 0; else if (o1 == null) return 1; else if (o2 == null) return -1; int priority = o1.getHexSideDrawPriority() - o2.getHexSideDrawPriority(); if (priority != 0) return priority; else return super.compare(o1, o2); } // see the comments for HexLine.draw(Graphics2D). @Override boolean draw(Graphics2D g) { LineDefinition l = getLine(); boolean result = false; if (l != null) { result = true; Point p = getPosition(); int size = getLayout().getHexSize(); int dX = getLayout().getDeltaX(); int dY = getLayout().getDeltaY(); if ((side & 0x1) > 0) { // vertical SW Point sw = getLayout().getSouthWest(hexIndex); sw.translate(dX, 0); Point s = getLayout().getSouth(hexIndex); l.addLine(p.x, sw.y, p.x + (size / 5), s.y); } if ((side & 0x2) > 0) { // vertical NW Point sw = getLayout().getSouthWest(hexIndex); sw.translate(dX, 0); l.addLine(p.x, sw.y, p.x + (size / 5), p.y); } if ((side & 0x4) > 0) { // vertical N l.addLine(p.x + (size / 5), p.y, p.x + dX, p.y); } if ((side & 0x8) > 0) { // horizontal SW Point se = getLayout().getSouthEast(hexIndex); l.addLine(p.x, p.y + dY, se.x, p.y + dY + (size / 5)); } if ((side & 0x10) > 0) { // horizontal W l.addLine(p.x, p.y + (size / 5), p.x, p.y + dY); } if ((side & 0x20) > 0) { // horizontal NW Point ne = getLayout().getNorthEast(hexIndex); l.addLine(p.x, p.y + (size / 5), ne.x, p.y); } if ((side & 0x40) > 0) { // square left l.addLine(p.x, p.y, p.x, p.y + dY); } if ((side & 0x80) > 0) { // square top l.addLine(p.x, p.y, p.x + dX, p.y); } } // if this is the last one, draw all the lines. if (this == hexSides.get(hexSides.size() - 1)) { drawLines(g, BasicStroke.CAP_ROUND); } return result; } @Override ArrayList<Line> getLineList(Hex h) { return h.hexSides; } } /** * Hexes aligned along rows. */ protected class HorizontalHexLayout extends HorizontalLayout { HorizontalHexLayout(int size, int columns, int rows) { super(size, columns, rows); } @Override Dimension getBoardSize() { Dimension d = new Dimension(); d.width = getDeltaX() * nColumns + getHexSize() / 2; d.height = getDeltaY() * nRows + getHexSize() / 5 + 1; return d; } @Override int getDeltaX() { return getHexSize() - (isPreV208Layout()?2:0); } @Override int getDeltaY() { return getHexSize() * 4 / 5 - 1; } @Override Point getOrigin() { return new Point(getHexSize() / 2, getHexSize() / 2 - (isPreV208Layout() ? 1 : 0)); } @Override HexGrid getGeometricGrid() { HexGrid mg = new HexGrid(); mg.setSideways(true); // VASSAL defines these sideways. Height always refers to the major // dimension, and Dy always refers to height whether they're sideways or not. mg.setOrigin(getOrigin()); mg.setDy(getDeltaX()); mg.setDx(getDeltaY()); return mg; } } /** * A layout consisting of squares in which every second row is shifted to the * right by one half-width. Used to approximate hexagons as each square has * six neighbours. * * @author Michael Kiefte * */ protected class GridOffsetRowLayout extends HorizontalLayout { GridOffsetRowLayout(int size, int columns, int rows) { super(size, columns, rows); } @Override Dimension getBoardSize() { Dimension d = new Dimension(); d.height = getDeltaY() * nRows + 1; d.width = getDeltaX() * nColumns + getHexSize()/2 + 1; return d; } @Override int getDeltaX() { return getHexSize(); } @Override int getDeltaY() { return getHexSize(); } @Override Point getOrigin() { return new Point(getHexSize() * 7 / 12, getHexSize() / 2); } @Override AbstractConfigurable getGeometricGrid() { HexGrid mg = new HexGrid(); mg.setSideways(true); mg.setOrigin(getOrigin()); mg.setDx(getDeltaY()); mg.setDy(getDeltaX()); return mg; } } /** * A layout in which every second row is offset by one-half hex or square. */ protected abstract class HorizontalLayout extends Layout { HorizontalLayout(int size, int columns, int rows) { super(size, columns, rows); } @Override int getNFaces() { return 6; } @Override void setGridNumberingOffsets(RegularGridNumbering numbering, MapSheet sheet) { Point position = coordinatesToPosition(sheet.getField().x, sheet.getField().y, true); position.translate(getDeltaX()/2, getDeltaY()/2); int rowOffset = numbering.getColumn(position); int colOffset = numbering.getRow(position); rowOffset = -rowOffset + sheet.getTopLeftRow(); colOffset = -colOffset + sheet.getTopLeftCol(); numbering.setAttribute(RegularGridNumbering.H_OFF, rowOffset); numbering.setAttribute(RegularGridNumbering.V_OFF, colOffset); } @Override void initGridNumbering(RegularGridNumbering numbering, MapSheet sheet) { super.initGridNumbering(numbering, sheet); boolean stagger = false; if (sheet.firstHexRight() && (sheet.getField().y&1) == 1) stagger = true; else if (sheet.firstHexLeft() && sheet.getField().y%2 == 0) stagger = true; numbering.setAttribute(HexGridNumbering.STAGGER, stagger); numbering.setAttribute(RegularGridNumbering.FIRST, sheet.rowsAndCols() ? "H" : "V"); numbering.setAttribute(RegularGridNumbering.H_TYPE, sheet.numericRows() ? "N" : "A"); numbering.setAttribute(RegularGridNumbering.V_TYPE, sheet.numericCols() ? "N" : "A"); numbering.setAttribute(RegularGridNumbering.H_LEADING, sheet.getNRowChars()-1); numbering.setAttribute(RegularGridNumbering.V_LEADING, sheet.getNColChars()-1); } @Override HexGridNumbering getGridNumbering() { return new HexGridNumbering(); } @Override Point coordinatesToPosition(int x, int y, boolean nullIfOffBoard) { if (!nullIfOffBoard || isOnMapBoard(x, y)) { int xx = getDeltaX() * x + (y % 2) * getDeltaX() / 2; int yy = getDeltaY() * y; return new Point(xx, yy); } else return null; } @Override Point getNorthEast(int index) { int row = getRow(index); int col = getCol(index) + Math.abs(row) % 2; --row; return coordinatesToPosition(col, row, false); } @Override Point getNorthWest(int index) { int row = getRow(index) - 1; int col = getCol(index) - Math.abs(row) % 2; return coordinatesToPosition(col, row, false); } @Override Rectangle getRectangle(MapSheet map) { Rectangle r = map.getField(); Point upperLeft = coordinatesToPosition(r.x, r.y, false); Point lowerRight = coordinatesToPosition(r.x + r.width - 1, r.y + r.height - 1, false); // adjust for staggering of hexes if (map.firstHexLeft()) // next one down is to the left upperLeft.x -= getHexSize() / 2; // adjust x of bottom right-hand corner if (r.y % 2 == (r.y + r.height - 1) % 2) { // both even or both odd if (map.firstHexRight()) lowerRight.x += getHexSize() / 2; // check to see if lower right-hand corner is on the wrong // square } else if ( (r.y&1) == 1 ) { // top is odd and bottom is even if (map.firstHexLeft()) lowerRight.x += getHexSize() / 2; else lowerRight.x += getHexSize(); } else if (map.firstHexLeft() && r.y % 2 == 0) // top is even and bottom is odd lowerRight.x -= getHexSize() / 2; // get lower right corner of lower right hex lowerRight.x += getHexSize() - 1; lowerRight.y += getHexSize() - 1; // adjust so that we don't overlap the centres of hexes that don't // belong to this sheet upperLeft.x += getHexSize() / 5; lowerRight.x -= getHexSize() / 5; constrainRectangle(upperLeft, lowerRight); return new Rectangle(upperLeft.x, upperLeft.y, lowerRight.x - upperLeft.x + 1, lowerRight.y - upperLeft.y + 1); } @Override Point getSouthEast(int index) { int row = getRow(index); int col = getCol(index) + Math.abs(row) % 2; ++row; return coordinatesToPosition(col, row, false); } @Override Point getSouthWest(int index) { int row = getRow(index) + 1; int col = getCol(index) - Math.abs(row) % 2; return coordinatesToPosition(col, row, false); } } /** * A drawable line such as a river, border, or road. */ protected abstract class Line extends MapDrawable { // index of line definition. don't know the actual line definitions until later private final int line; Line(int index, int line) { super(index); this.line = line; if (hexes == null) hexes = new Hex[getNColumns() * getNRows()]; if (hexes[index] == null) hexes[index] = new Hex(); getLineList(hexes[index]).add(this); } /** * @return The <code>LineDefinition</code> for this line. */ LineDefinition getLine() { return getLineDefinition(line); } /** * @return The list of lines by hex. */ abstract ArrayList<Line> getLineList(Hex h); // I no longer remember how this works, but I do remember it took a long time to figure out. int compare(LineDefinition o1, LineDefinition o2) { if (o1 == null && o2 == null) return 0; else if (o1 == null) return 1; else if (o2 == null) return -1; // go through all the hexes // and determine file order for lines for (Hex h : hexes) { if (h == null) continue; boolean index1 = false; boolean index2 = false; for (Line hl : getLineList(h)) { if (hl.getLine() == o1) { if (index2) return 1; index1 = true; } else if (hl.getLine() == o2) { if (index1) return -1; index2 = true; } } } return 0; } void drawLines(Graphics2D g, int cap) { final ArrayList<LineDefinition> lds = new ArrayList<LineDefinition>(Arrays.asList(lineDefinitions)); // find the next line in priority while (lds.size() > 0) { LineDefinition lowest = null; for (LineDefinition ld : lds) { if (ld == null) continue; else if (lowest == null || compare(ld, lowest) < 0) lowest = ld; } if (lowest == null) break; else { lowest.draw(g, cap); lowest.clearPoints(); lds.remove(lowest); } } } } /** * Line styles for hex sides and hex lines. */ protected static class LineDefinition { private final Color color; private int hexLineDrawPriority; private int hexSideDrawPriority; // using floats because we really want to aim for the centre pixel, not necessarily // the space between pixels--only important for aliasing effects. private ArrayList<ArrayList<Point2D.Float>> points = new ArrayList<ArrayList<Point2D.Float>>(); // line width private final int size; private final LineStyle style; LineDefinition(Color color, int size, MapBoard.LineStyle style) { this.color = color; this.size = size; this.style = style; } private void setHexLineDrawPriority(int priority) { // only change the priority if it hasn't already been set. if (hexLineDrawPriority == 0) hexLineDrawPriority = priority; } private void setHexSideDrawPriority(int priority) { if (hexSideDrawPriority == 0) hexSideDrawPriority = priority; } Color getColor() { return color; } BasicStroke getStroke(int cap) { if (size <= 0 || style == null) return null; return style.getStroke(size, cap); } void addLine(float x1, float y1, float x2, float y2) { addLine(new Point2D.Float(x1, y1), new Point2D.Float(x2, y2)); } void addLine(int x1, int y1, float x2, float y2) { addLine(new Point2D.Float((float) x1, (float) y1), new Point2D.Float(x2, y2)); } void addLine(int x1, int y1, int x2, int y2) { addLine(new Point2D.Float((float) x1, (float) y1), new Point2D.Float((float) x2, (float) y2)); } /** * Add a line to the line list for later processing. Attach it to already existing line if possible. * Otherwise start a new one. */ void addLine(Point2D.Float a, Point2D.Float b) { // find out if this line is attached to any other line in the list. // if not create a line. for (int i = 0; i < points.size(); ++i) { ArrayList<Point2D.Float> lineA = points.get(i); if (a.equals(lineA.get(0))) { // a at the start of lineA // repeated segment? if (b.equals(lineA.get(1))) return; // find out if this segment joins two lines already in // existance for (int j = 0; j < points.size(); ++j) { if (i == j) continue; ArrayList<Point2D.Float> lineB = points.get(j); if (b.equals(lineB.get(0))) { // point A at start of lineA and point B at start of lineB if (lineA.size() < lineB.size()) { // insert A before B for (int k = 0; k < lineA.size(); ++k) lineB.add(0, lineA.get(k)); points.remove(i); } else { // insert B before A for (int k = 0; k < lineB.size(); ++k) lineA.add(0, lineB.get(k)); points.remove(j); } return; } else if (b.equals(lineB.get(lineB.size() - 1))) { // point A at start of lineA and point B at end of lineB lineB.addAll(lineA); points.remove(i); return; } } // point A at start of lineA and point B is open lineA.add(0, b); return; } else if (a.equals(lineA.get(lineA.size() - 1))) { // Point A is at end of line A if (b.equals(lineA.get(lineA.size() - 2))) // repeated segment? return; for (int j = 0; j < points.size(); ++j) { if (i == j) // skip closed loops continue; ArrayList<Point2D.Float> lineB = points.get(j); if (b.equals(lineB.get(0))) { // point A at end of line A and point B at start of lineB lineA.addAll(lineB); points.remove(j); return; } else if (b.equals(lineB.get(lineB.size() - 1))) { // point A at end of lineA and point B at end of lineB if (lineA.size() < lineB.size()) { // add line A to B for (int k = lineA.size() - 1; k >= 0; --k) lineB.add(lineA.get(k)); points.remove(i); } else { // add line B to A for (int k = lineB.size() - 1; k >= 0; --k) lineA.add(lineB.get(k)); points.remove(j); } return; } } // point A at the end of lineA and point B is open lineA.add(b); return; } // find out if the segment already exists for (int j = 1; j < lineA.size() - 1; ++j) if (a.equals(lineA.get(j)) && (b.equals(lineA.get(j - 1)) || b.equals(lineA .get(j + 1)))) return; } // point A is open (not attached) for (ArrayList<Point2D.Float> line : points) { if (b.equals(line.get(0))) { // B at the start of the line // repeated segment? if (a.equals(line.get(1))) return; line.add(0, a); return; } else if (b.equals(line.get(line.size() - 1))) { // B at the end of the line if (a.equals(line.get(line.size() - 2))) return; line.add(a); return; } } // both A and B are open ArrayList<Point2D.Float> newLine = new ArrayList<Point2D.Float>(2); newLine.add(a); newLine.add(b); points.add(newLine); } /** * start fresh. */ void clearPoints() { points.clear(); } void draw(Graphics2D g, int cap) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); BasicStroke stroke = getStroke(cap); if (stroke == null) return; g.setStroke(stroke); g.setColor(getColor()); GeneralPath gp = new GeneralPath(GeneralPath.WIND_EVEN_ODD); for (ArrayList<Point2D.Float> line : points) { gp.moveTo(line.get(0).x, line.get(0).y); for (Point2D.Float p : line) { if (!p.equals(line.get(0))) gp.lineTo(p.x, p.y); else if (p != line.get(0)) gp.closePath(); } } g.draw(gp); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); } int getHexLineDrawPriority() { return hexLineDrawPriority; } int getHexSideDrawPriority() { return hexSideDrawPriority; } } /** * line patter such as dashed or dotted or solid */ protected enum LineStyle { DASH_DOT(new float[] { 12.0f, 8.0f, 4.0f, 8.0f }), DASH_DOT_DOT(new float[] { 12.f, 4.0f, 4.0f, 4.0f, 4.0f, 4.0f }), DASHED(new float[] { 12.0f, 8.0f }), DOTTED(new float[] { 4.0f, 4.0f }), SOLID(null); private float[] dash; LineStyle(float[] dash) { this.dash = dash; } BasicStroke getStroke(int size, int cap) { if (dash == null) // nice effect if it's a solid line return new BasicStroke(size, cap, BasicStroke.JOIN_ROUND); else return new BasicStroke(size, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 0.0f, dash, 0.0f); } } /** * Anything that can be drawn on the map and is associated with a particular hex. */ protected abstract class MapDrawable { // hex index in row-major order: determines location on map protected final int hexIndex; MapDrawable(int index) { this.hexIndex = index; } /** * Draw the element to the graphics context. Return <code>true</code> if an element was actually drawn. */ abstract boolean draw(Graphics2D g); int getHexIndex() { return hexIndex; } /** * @return Upper left-hand corner of hex or square. */ Point getPosition() { return indexToPosition(hexIndex); } /** * @return Enclosing rectangle for hex or square. */ Rectangle getRectangle() { Rectangle r = new Rectangle(getPosition()); int width = getLayout().getHexSize(); r.width = width; r.height = width; return r; } } /** * Defines the numbering system for an ADC2 mapboard. */ protected class MapSheet { private int topLeftCol; private int topLeftRow; private final Rectangle field; private final String name; private final int nColChars; private final int nRowChars; private final int style; private Zone zone; MapSheet(String name, Rectangle playingFieldPosition, int style, int nColChars, int nRowChars) { this.name = name; this.field = playingFieldPosition; this.style = style; this.nColChars = nColChars; this.nRowChars = nRowChars; } /** * @return A rectangle giving the bounds of the hex coordinates. */ Rectangle getField() { return field; } /** * @return The sheet name. */ String getName() { return name; } /** * @return Number of characters in the column label. */ int getNColChars() { return nColChars; } /** * @return Number of characters in the row label. */ int getNRowChars() { return nRowChars; } /** * Used by the grid numbering system in VASSAL. */ String getRectangleAsString() { Rectangle r = getLayout().getRectangle(this); if (r == null) return null; return r.x + "," + r.y + ";" + (r.x + r.width - 1) + "," + r.y + ";" + (r.x + r.width - 1) + "," + (r.y + r.height - 1) + ";" + r.x + "," + (r.y + r.height - 1); } /** * @return <code>true</code> if column labels are alphabetic. */ boolean alphaCols() { return !numericCols(); } /** * @return <code>true</code> if row labels are alphabetic. */ boolean alphaRows() { return !numericRows(); } /** * @return <code>true</code> if columns are first in coordinate label. */ boolean colsAndRows() { return (style & 0x2) > 0; } /** * @return <code>true</code> if column labels increase going left. */ boolean colsIncreaseLeft() { return !colsIncreaseRight(); } /** * @return <code>true</code> if column labels increase going right. */ boolean colsIncreaseRight() { return (style & 0x10) > 0; } /** * @return <code>true</code> if the row label of odd-numbered columns is shifted down. */ boolean firstHexDown() { return (style & 0x40) > 0 && getLayout() instanceof VerticalLayout; } /** * @return <code>true</code> if the row label of odd-numbered rows is shifted left. */ boolean firstHexLeft() { return (style & 0x40) > 0 && getLayout() instanceof HorizontalLayout; } /** * @return <code>true</code> if the row label of odd-numbered rows is shifted right. */ boolean firstHexRight() { return (style & 0x40) == 0 && getLayout() instanceof HorizontalLayout; } /** * @return <code>true</code> if the row label of odd-numbered columns is shifted down. */ boolean firstHexUp() { return (style & 0x40) == 0 && getLayout() instanceof VerticalLayout; } RegularGridNumbering getGridNumbering() { // numbering system RegularGridNumbering gn = getLayout().getGridNumbering(); getLayout().initGridNumbering(gn, this); return gn; } /** * Creates a VASSAL <code>Zone</code> if not already created and initializes settings. */ Zone getZone() { if (zone == null) { zone = new Zone(); zone.setConfigureName(getName()); String rect = getRectangleAsString(); if (rect == null) return null; zone.setAttribute(Zone.PATH, rect); zone.setAttribute(Zone.LOCATION_FORMAT, "$name$ $gridLocation$"); AbstractConfigurable mg = getLayout().getGeometricGrid(); // add numbering system to grid RegularGridNumbering gn = getGridNumbering(); insertComponent(gn, mg); // add grid to zone insertComponent(mg, zone); getLayout().setGridNumberingOffsets(gn, this); } return zone; } /** * @return <code>true</code> if column labels are numeric. */ boolean numericCols() { return (style & 0x4) > 0; } /** * @return <code>true</code> if row labels are numeric. */ boolean numericRows() { return (style & 0x8) > 0; } /** * Rows before columns? */ boolean rowsAndCols() { return !colsAndRows(); } /** * @return <code>true</code> if row labels increase downward. */ boolean rowsIncreaseDown() { return (style & 0x20) > 0; } /** * @return <code>true</code> if row labels increase upward. */ boolean rowsIncreaseUp() { return !rowsIncreaseDown(); } /** * @return Index of the top left column on the map sheet. */ int getTopLeftCol() { return topLeftCol; } /** * Sets the top left column index of the map sheet. */ void setTopLeftCol(int topLeftCol) { this.topLeftCol = topLeftCol; } /** * @return Index of the top left row of the map sheet. */ int getTopLeftRow() { return topLeftRow; } /** * Sets the top left row index of the map sheet. */ void setTopLeftRow(int topLeftRow) { this.topLeftRow = topLeftRow; } } /** * Place name element which includes not only the name itself, but the font and style that it * should be drawn with. */ protected class PlaceName extends MapDrawable { // text colour private final Color color; // bit flags private final int font; // position relative to the hex. not really orientation. e.g., can't // have vertical text. private final PlaceNameOrientation orientation; // font size private final int size; // the actual name private final String text; PlaceName(int index, String text, Color color, PlaceNameOrientation orientation, int size, int font) { super(index); this.text = text; assert (color != null); this.color = color; assert (orientation != null); this.orientation = orientation; // assert (size > 0); this.size = size; font &= 0x7f; int fontIndex = font & 0xf; if (fontIndex < 1 || fontIndex > 9) { fontIndex = 9; font = font & 0xf0 | fontIndex; } this.font = font; } Font getFont() { int size = getSize(); return size == 0 ? null : getDefaultFont(getSize(), font); } /** * Get the position based on the hex index, font and orientation. */ Point getPosition(Graphics2D g) { Point p = getPosition(); if (getSize() == 0) return p; assert (g.getFont() == getFont()); FontMetrics fm = g.getFontMetrics(); int size = getLayout().getHexSize(); switch (orientation) { case LOWER_CENTER: case UPPER_CENTER: case LOWER_RIGHT: case UPPER_RIGHT: case UPPER_LEFT: case LOWER_LEFT: case HEX_CENTER: p.x += size / 2; // middle of the hex. break; case CENTER_RIGHT: p.x += size; // right of hex break; case CENTER_LEFT: break; } switch (orientation) { case LOWER_CENTER: case UPPER_CENTER: case HEX_CENTER: // text centered p.x -= fm.charsWidth(text.toCharArray(), 0, text.length()) / 2; break; case UPPER_LEFT: case LOWER_LEFT: case CENTER_LEFT: // right justified p.x -= fm.charsWidth(text.toCharArray(), 0, text.length()); break; case LOWER_RIGHT: case UPPER_RIGHT: case CENTER_RIGHT: break; } switch (orientation) { case LOWER_CENTER: case LOWER_RIGHT: case LOWER_LEFT: p.y += size + fm.getAscent(); break; case UPPER_CENTER: case UPPER_RIGHT: case UPPER_LEFT: p.y -= fm.getDescent(); break; case CENTER_LEFT: case CENTER_RIGHT: case HEX_CENTER: p.y += size / 2 + fm.getHeight() / 2 - fm.getDescent(); break; } return p; } // scale the size more appropriately--for some reason ADC2 font sizes // don't correspond to anything else. int getSize() { return size <= 5 ? 0 : (size + 1) * 4 / 3 - 1; } @Override boolean draw(Graphics2D g) { if (getSize() != 0) { g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g.setFont(getFont()); g.setColor(color); Point p = getPosition(g); g.drawString(text, p.x, p.y); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); return true; } else { return false; } } String getText() { return text; } } protected enum PlaceNameOrientation { CENTER_LEFT, CENTER_RIGHT, HEX_CENTER, LOWER_CENTER, LOWER_LEFT, LOWER_RIGHT, UPPER_CENTER, UPPER_LEFT, UPPER_RIGHT; } /** * A layout consisting of squares in which every second column is shifted * downward by one half width. This is done to approximate hexagons as each * square as six neighbours. * * @author Michael Kiefte * */ protected class GridOffsetColumnLayout extends VerticalLayout { GridOffsetColumnLayout(int size, int columns, int rows) { super(size, columns, rows); } @Override Dimension getBoardSize() { Dimension d = new Dimension(); d.width = getDeltaX() * nColumns + 1; d.height = getDeltaY() * nRows + getHexSize() / 2 + 1; return d; } @Override int getDeltaX() { return getHexSize(); } @Override int getDeltaY() { return getHexSize(); } @Override Point getOrigin() { return new Point(getHexSize() * 7 / 12, getHexSize() / 2); } @Override AbstractConfigurable getGeometricGrid() { HexGrid mg = new HexGrid(); mg.setOrigin(getOrigin()); mg.setDx(getDeltaX()); mg.setDy(getDeltaY()); return mg; } } /** * Hexes in columns. */ protected class VerticalHexLayout extends VerticalLayout { VerticalHexLayout(int size, int columns, int rows) { super(size, columns, rows); } @Override Dimension getBoardSize() { Dimension d = new Dimension(); d.width = getDeltaX() * nColumns + getHexSize() / 5 + 1; d.height = getDeltaY() * nRows + getHexSize() / 2 + 1; return d; } @Override int getDeltaX() { return getHexSize() * 4 / 5 - (isPreV208Layout() ? 1 : 0); } @Override int getDeltaY() { return getHexSize() - (isPreV208Layout() ? 2 : 1); } @Override Point getOrigin() { return new Point(getHexSize() / 2, getHexSize() / 2 - (isPreV208Layout() ? 1 : 0)); } @Override HexGrid getGeometricGrid() { HexGrid mg = new HexGrid(); mg.setOrigin(getOrigin()); mg.setDx(getDeltaX()); mg.setDy(getDeltaY()); return mg; } } /** * A layout in which every second column is offset--either hexes or squares. */ protected abstract class VerticalLayout extends Layout { @Override int getNFaces() { return 6; } VerticalLayout(int size, int columns, int rows) { super(size, columns, rows); } @Override HexGridNumbering getGridNumbering() { return new HexGridNumbering(); } @Override void initGridNumbering(RegularGridNumbering numbering, MapSheet sheet) { boolean stagger = false; if (sheet.firstHexDown() && (sheet.getField().x&1) == 1) stagger = true; else if (sheet.firstHexUp() && sheet.getField().x%2 == 0) stagger = true; numbering.setAttribute(HexGridNumbering.STAGGER, stagger); super.initGridNumbering(numbering, sheet); } @Override Point coordinatesToPosition(int x, int y, boolean nullIfOffBoard) { if (!nullIfOffBoard || isOnMapBoard(x, y)) { int xx = getDeltaX() * x; int yy = getDeltaY() * y + x % 2 * getDeltaY() / 2; return new Point(xx, yy); } else return null; } @Override Point getNorthEast(int index) { int col = getCol(index) + 1; int row = getRow(index) - Math.abs(col) % 2; return coordinatesToPosition(col, row, false); } @Override Point getNorthWest(int index) { int col = getCol(index) - 1; int row = getRow(index) - Math.abs(col) % 2; return coordinatesToPosition(col, row, false); } @Override Rectangle getRectangle(MapSheet map) { Rectangle r = map.getField(); if (r.width <= 0 || r.height <= 0) return null; Point upperLeft = coordinatesToPosition(r.x, r.y, false); Point lowerRight = coordinatesToPosition(r.x + r.width - 1, r.y + r.height - 1, false); // adjust for staggering of hexes if (map.firstHexUp()) // next one over is above upperLeft.y -= getHexSize() / 2; // adjust y of bottom right-hand corner if (r.x % 2 == (r.x + r.width - 1) % 2) { // both even or both odd if (map.firstHexDown()) lowerRight.y += getHexSize() / 2; // check to see if lower right-hand corner is on the wrong // square } else if ( (r.x&1) == 1) { // left is odd and right is even if (map.firstHexDown()) lowerRight.y += getHexSize(); else lowerRight.y += getHexSize()/2; } else if (map.firstHexUp() && r.x % 2 == 0) { // left is even and right is odd lowerRight.y -= getHexSize() / 2; } // get lower right corner of lower right hex lowerRight.x += getHexSize() - 1; lowerRight.y += getHexSize() - 1; // adjust so that we don't overlap the centres of hexes that don't // belong to this sheet upperLeft.y += getHexSize() / 5; lowerRight.y -= getHexSize() / 5; constrainRectangle(upperLeft, lowerRight); return new Rectangle(upperLeft.x, upperLeft.y, lowerRight.x - upperLeft.x + 1, lowerRight.y - upperLeft.y + 1); } @Override Point getSouthEast(int index) { int col = getCol(index); int row = getRow(index) + Math.abs(col) % 2; ++col; return coordinatesToPosition(col, row, false); } @Override Point getSouthWest(int index) { int col = getCol(index); int row = getRow(index) + Math.abs(col) % 2; --col; return coordinatesToPosition(col, row, false); } } /** * How the hexes or squares are organized on the map board. */ protected abstract class Layout { protected final int nColumns; protected final int nRows; // Size of the hexes or squares. private final int size; Layout(int size, int columns, int rows) { this.size = size; this.nColumns = columns; this.nRows = rows; } protected int getRow(int index) { return index / nColumns; } protected int getCol(int index) { return index % nColumns; } /** * Move the upper left and lower-right points to just within the map board. */ void constrainRectangle(Point upperLeft, Point lowerRight) { if (upperLeft.x < 0) upperLeft.x = 0; if (upperLeft.y < 0) upperLeft.y = 0; Dimension d = getBoardSize(); if (lowerRight.x >= d.width) lowerRight.x = d.width-1; if (lowerRight.y >= d.height) lowerRight.y = d.height-1; } /** * @return number of flat sides. <i>e.g.</i>, four for squares, six for hexes. */ abstract int getNFaces(); /** * Set attributes of the <code>GridNumbering</code> object based on map board parameters. */ void initGridNumbering(RegularGridNumbering numbering, MapSheet sheet) { numbering.setAttribute(RegularGridNumbering.FIRST, sheet.colsAndRows() ? "H" : "V"); numbering.setAttribute(RegularGridNumbering.H_TYPE, sheet.numericCols() ? "N" : "A"); numbering.setAttribute(RegularGridNumbering.H_LEADING, sheet.getNColChars()-1); numbering.setAttribute(RegularGridNumbering.H_DESCEND, sheet.colsIncreaseLeft()); numbering.setAttribute(RegularGridNumbering.H_DESCEND, sheet.colsIncreaseLeft()); numbering.setAttribute(RegularGridNumbering.V_TYPE, sheet.numericRows() ? "N" : "A"); numbering.setAttribute(RegularGridNumbering.V_LEADING, sheet.getNRowChars()-1); numbering.setAttribute(RegularGridNumbering.V_DESCEND, sheet.rowsIncreaseUp()); } /** * Set the offset in the grid numbering system according to the specified map sheet. */ void setGridNumberingOffsets(RegularGridNumbering numbering, MapSheet sheet) { Point position = coordinatesToPosition(sheet.getField().x, sheet.getField().y, true); // shift to the middle of the hex position.translate(getDeltaX()/2, getDeltaY()/2); // use the numbering system to find out where we are int rowOffset = numbering.getRow(position); int colOffset = numbering.getColumn(position); rowOffset = -rowOffset + sheet.getTopLeftRow(); colOffset = -colOffset + sheet.getTopLeftCol(); numbering.setAttribute(RegularGridNumbering.H_OFF, colOffset); numbering.setAttribute(RegularGridNumbering.V_OFF, rowOffset); } /** * @return an uninitialized grid numbering system appropriate for this layout */ abstract RegularGridNumbering getGridNumbering(); /** * Returns a point corresponding the the upper-left corner of the square * specified by the coordinates. * * @param x column * @param y row * @param nullIfOffBoard return null if not on the board. Otherwise the point * may not be valid. * @return the point corresponding to the upper-left-hand corner of the square. */ abstract Point coordinatesToPosition(int x, int y, boolean nullIfOffBoard); /** * @return board image size dimensions in pixels. */ abstract Dimension getBoardSize(); /** * @return the distance in pixels to the next square on the right. */ abstract int getDeltaX(); /** * @return the distance in pixels ot the next square below */ abstract int getDeltaY(); /** * Returns the location of the hex or square to the East. * * @param index raw index (columns increasing fastest). * @return the position in pixels of the next hex or square to the East. */ Point getEast(int index) { int row = getRow(index); int col = getCol(index) + 1; return coordinatesToPosition(col, row, false); } /** * @return an initialized VASSAL hex grid appropriate for the current layout */ abstract AbstractConfigurable getGeometricGrid(); /** * Returns the location of the hex or square to the North. * * @param index raw index (columns increasing fastest). * @return the position in pixels of the next hex or square to the North. */ Point getNorth(int index) { int row = getRow(index) - 1; int col = getCol(index); return coordinatesToPosition(col, row, false); } /** * Returns the location of the hex or square to the NorthEast. * * @param index raw index (columns increasing fastest). * @return the position in pixels of the next hex or square to the NorthEast. */ Point getNorthEast(int index) { int row = getRow(index) - 1; int col = getCol(index) + 1; return coordinatesToPosition(col, row, false); } /** * Returns the location of the hex or square to the NorthWest. * * @param index raw index (columns increasing fastest). * @return the position in pixels of the next hex or square to the NorthWest. */ Point getNorthWest(int index) { int row = getRow(index) - 1; int col = getCol(index) - 1; return coordinatesToPosition(col, row, false); } /** * @return the centre in pixels of a square or hex relative to the top-left corner. */ abstract Point getOrigin(); /** * Returns a rectangle in pixels that encloses the given <code>MapSheet</code>. * Returns null if <code>MapSheet</code> has a negative size. */ abstract Rectangle getRectangle(MapSheet map); /** * @return the size of the hexes or squares in pixels. */ int getHexSize() { return size; } /** * Returns the location of the hex or square to the South. * * @param index raw index (columns increasing fastest). * @return the position in pixels of the next hex or square to the South. */ Point getSouth(int index) { int row = getRow(index) + 1; int col = getCol(index); return coordinatesToPosition(col, row, false); } /** * Returns the location of the hex or square to the SouthEast. * * @param index raw index (columns increasing fastest). * @return the position in pixels of the next hex or square to the SouthEast. */ Point getSouthEast(int index) { int row = getRow(index) + 1; int col = getCol(index) + 1; return getLayout().coordinatesToPosition(col, row, false); } /** * Returns the location of the hex or square to the SouthWest. * * @param index raw index (columns increasing fastest). * @return the position in pixels of the next hex or square to the SouthWest. */ Point getSouthWest(int index) { int row = getRow(index) + 1; int col = getCol(index) - 1; return coordinatesToPosition(col, row, false); } /** * Returns the location of the hex or square to the West. * * @param index raw index (columns increasing fastest). * @return the position in pixels of the next hex or square to the West. */ Point getWest(int index) { int row = getRow(index); int col = getCol(index) - 1; return coordinatesToPosition(col, row, false); } } // Archive of fonts used for placenames. makes reuse possible and is // probably faster as most of the place names use only one of a very few fonts. private final static HashMap<Integer, Font> defaultFonts = new HashMap<Integer, Font>(); // which level to import private static final int zoomLevel = 2; // fonts available to ADC private static final String[] defaultFontNames = { "Courier", "Fixedsys", "MS Sans Serif", "MS Serif", "Impact", "Brush Script MT", "System", "Times New Roman", "Arial" }; private static final String PLACE_NAMES = "Place Names"; /** * Get a font based on size and font index. If this font has not already been created, then it will be generated. * Can be reused later if the same font was already created. * * @param size Font size. * @param font Font index. See MapBoard.java for format. */ /* Binary format for fonts: * * 00000000 * ||||_ Font name index (between 1 and 9). * |_____ Bold flag. * |______ Italics flag. * |_______ Underline flag. */ protected static Font getDefaultFont(int size, int font) { final Integer key = Integer.valueOf((size << 8) + font); Font f = defaultFonts.get(key); if (f == null) { int fontIndex = font & 0xf; assert (fontIndex >= 1 && fontIndex <= 9); boolean isBold = (font & 0x0010) > 0; boolean isItalic = (font & 0x0020) > 0; boolean isUnderline = (font & 0x0040) > 0; String fontName = defaultFontNames[fontIndex - 1]; int fontStyle = Font.PLAIN; if (isItalic) fontStyle |= Font.ITALIC; if (isBold) fontStyle |= Font.BOLD; f = new Font(fontName, fontStyle, size); if (isUnderline) { // TODO: why doesn't underlining doesn't work? Why why why? Hashtable<TextAttribute, Object> hash = new Hashtable<TextAttribute, Object>(); hash.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); f = f.deriveFont(hash); } defaultFonts.put(key, f); } return f; } // tertiary symbols. private final ArrayList<HexData> attributes = new ArrayList<HexData>(); // name of the map board is derived from the file name private String baseName; // hex data organized by index private Hex[] hexes; // hexline data private final ArrayList<HexLine> hexLines = new ArrayList<HexLine>(); // hexside data private final ArrayList<HexSide> hexSides = new ArrayList<HexSide>(); // layout of the hexes or squares private Layout layout; // line definitions needed for hex sides and lines private LineDefinition[] lineDefinitions; // organizes all the drawable elements in order of drawing priority private ArrayList<MapLayer> mapElements = new ArrayList<MapLayer>(); // grid numbering systems private final ArrayList<MapSheet> mapSheets = new ArrayList<MapSheet>(); // labels; not necessary actual places corresponding to a hex, although // that's how it's described by ADC2 private final ArrayList<PlaceName> placeNames = new ArrayList<PlaceName>(); // optional place symbol in addition to primary and secondary mapboard // symbol private final ArrayList<HexData> placeSymbols = new ArrayList<HexData>(); // primary mapboard symbols. Every hex must have one even if it's null. private final ArrayList<HexData> primaryMapBoardSymbols = new ArrayList<HexData>(); // and secondary mapboard symbols (typically a lot fewer) private final ArrayList<HexData> secondaryMapBoardSymbols = new ArrayList<HexData>(); // overlay symbol. there's only one, but we make it an ArrayList<> for consistency // with other drawing objects private final ArrayList<MapBoardOverlay> overlaySymbol = new ArrayList<MapBoardOverlay>(); // symbol set associated with this map -- needed for mapboard symbols private SymbolSet set; // How many hex columns in the map. private int columns; // How many hex rows in the map. private int rows; // background map color when hexes are not drawn. private Color tableColor; // version information needed for rendering hexes and determining hex dimensions private boolean isPreV208 = true; // map file path private String path; // The VASSAL BoardPicker object which is the tree parent of Board. private BoardPicker boardPicker; private byte[] drawingPriorities = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // initialize the drawing elements which must all be ArrayList<>'s. public MapBoard() { mapElements.add(new MapLayer(primaryMapBoardSymbols, "Primary MapBoard Symbols", false)); mapElements.add(new MapLayer(secondaryMapBoardSymbols, "Secondary MapBoard Symbols", false)); mapElements.add(new MapLayer(hexSides, "Hex Sides", true)); mapElements.add(new MapLayer(hexLines, "Hex Lines", true)); mapElements.add(new MapLayer(placeSymbols, "Place Symbols", false)); mapElements.add(new MapLayer(attributes, "Attributes", false)); mapElements.add(new MapLayer(overlaySymbol, "Overlay Symbol", true)); mapElements.add(new MapLayer(placeNames, "Place Names", true)); } /** * Many maps are actually just scanned images held in a separate file. The images are often broken up into * sections. An extra file describes how the images are pasted together. This function pieces the images * together using the <code>Graphics2D</code> object <code>g</code>. */ protected void readScannedMapLayoutFile(File f, Graphics2D g) throws IOException { DataInputStream in = null; try { in = new DataInputStream(new BufferedInputStream(new FileInputStream(f))); // how many image sections int nSheets = ADC2Utils.readBase250Word(in); for (int i = 0; i < nSheets; ++i) { // image file name String name = stripExtension(readWindowsFileName(in)); File file = action.getCaseInsensitiveFile(new File(name + "-L" + (zoomLevel+1) + ".bmp"), new File(path), true, null); if (file == null) throw new FileNotFoundException("Unable to find map image."); BufferedImage img = ImageIO.read(file); int x = 0; int y = 0; for (int j = 0; j < 3; ++j) { int tempx = ADC2Utils.readBase250Integer(in); int tempy = ADC2Utils.readBase250Integer(in); if (j == zoomLevel) { x = tempx; y = tempy; } } g.drawImage(img, null, x, y); } in.close(); } finally { IOUtils.closeQuietly(in); } } /** * Read symbol that is drawn on all hexes. */ protected void readMapBoardOverlaySymbolBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "MapBoard Overlay Symbol"); SymbolSet.SymbolData overlaySymbol = getSet().getMapBoardSymbol(ADC2Utils.readBase250Word(in)); // the reason we use an ArrayList<> here is so that it is treated like all other drawn elements if (overlaySymbol != null) this.overlaySymbol.add(new MapBoardOverlay(overlaySymbol)); } /** * Block of flags to indicate which elements are actually displayed. */ protected void readMapItemDrawFlagBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Map Item Draw Flag"); // obviously, element types can't be sorted before we do this. // TODO: check this! If they're turned off in the map, can they be turned on // again in the player? final ArrayList<MapLayer> elements = new ArrayList<MapLayer>(mapElements); if (in.readByte() == 0) mapElements.remove(elements.get(drawingPriorities[0])); if (in.readByte() == 0) mapElements.remove(elements.get(drawingPriorities[1])); if (in.readByte() == 0) mapElements.remove(elements.get(drawingPriorities[2])); if (in.readByte() == 0) mapElements.remove(elements.get(drawingPriorities[3])); if (in.readByte() == 0) mapElements.remove(elements.get(drawingPriorities[4])); if (in.readByte() == 0) mapElements.remove(elements.get(drawingPriorities[7])); if (in.readByte() == 0) mapElements.remove(elements.get(drawingPriorities[5])); } /** * Attributes are tertiary symbols, any number of which can be attached to a specific hex. Otherwise, they * function the same as primary or secondary hex symbols. */ protected void readAttributeBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Attribute Symbol"); int nAttributes = ADC2Utils.readBase250Word(in); for (int i = 0; i < nAttributes; ++i) { int index = ADC2Utils.readBase250Word(in); SymbolSet.SymbolData symbol = set.getMapBoardSymbol(ADC2Utils.readBase250Word(in)); if (isOnMapBoard(index) && symbol != null) attributes.add(new HexData(index, symbol)); } } /** * Read primary and secondary symbol information. Each hex may only have one of each. Additional symbols must * be tertiary attributes. */ protected void readHexDataBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Hex Data"); int count = getNColumns() * getNRows(); for (int i = 0; i < count; ++i) { // primary symbol int symbolIndex = ADC2Utils.readBase250Word(in); SymbolSet.SymbolData symbol = getSet().getMapBoardSymbol(symbolIndex); if (symbol != null) primaryMapBoardSymbols.add(new HexData(i, symbol)); // secondary symbol symbolIndex = ADC2Utils.readBase250Word(in); symbol = getSet().getMapBoardSymbol(symbolIndex); if (symbol != null) secondaryMapBoardSymbols.add(new HexData(i, symbol)); /* int elevation = */ ADC2Utils.readBase250Word(in); // flags for hexsides, lines, and placenames: completely ignored /* int additionalInformation = */ in.readUnsignedByte(); } } /** * Hex lines are like spokes of the hex and are typically used for things like roads or other elements that * traverse from hex to hex. The direction of each spoke is encoded as bit flags, and while ADC2 could encode * each hex with only one record, modules typically have a separate record for every spoke resulting in * data inflation. */ protected void readHexLineBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Hex Line"); int nHexLines = ADC2Utils.readBase250Word(in); for (int i = 0; i < nHexLines; ++i) { int index = ADC2Utils.readBase250Word(in); int line = in.readUnsignedByte(); int direction = in.readUnsignedShort(); if (isOnMapBoard(index)) hexLines.add(new HexLine(index, line, direction)); } } /** * Information about hex sides which are used for things like rivers, etc. is read in. */ protected void readHexSideBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Hex Side"); int nHexSides = ADC2Utils.readBase250Word(in); for (int i = 0; i < nHexSides; ++i) { int index = ADC2Utils.readBase250Word(in); int line = in.readUnsignedByte(); int side = in.readUnsignedByte(); if (isOnMapBoard(index)) hexSides.add(new HexSide(index, line, side)); } } /** * Information about the width, colour and style of hex sides and hex lines is read in. */ protected void readLineDefinitionBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Line Definition"); int nLineDefinitions = in.readUnsignedByte(); lineDefinitions = new LineDefinition[nLineDefinitions]; for (int i = 0; i < nLineDefinitions; ++i) { int colorIndex = in.readUnsignedByte(); Color color = ADC2Utils.getColorFromIndex(colorIndex); int size = 0; for (int j = 0; j < 3; ++j) { int s = in.readByte(); if (j == zoomLevel) size = s; } // only used when editing ADC2 maps within ADC2. /* String name = */ readNullTerminatedString(in, 25); int styleByte = in.readUnsignedByte(); LineStyle style; switch (styleByte) { case 2: style = LineStyle.DOTTED; break; case 3: style = LineStyle.DASH_DOT; break; case 4: style = LineStyle.DASHED; break; case 5: style = LineStyle.DASH_DOT_DOT; break; default: style = LineStyle.SOLID; } if (size > 0) lineDefinitions[i] = new LineDefinition(color, size, style); else lineDefinitions[i] = null; } } /** * Read what order to draw the lines in. */ protected void readLineDrawPriorityBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Line Draw Priority"); in.readByte(); // unused. // there can only be 10 line definitions. however drawning priorities for hex sides and hex lines // are completely independent for (int i = 1; i <= 10; ++i) { int index = in.readUnsignedByte(); if (index < lineDefinitions.length && lineDefinitions[index] != null) lineDefinitions[index].setHexLineDrawPriority(i); } for (int i = 1; i <= 10; ++i) { int index = in.readUnsignedByte(); if (index < lineDefinitions.length && lineDefinitions[index] != null) lineDefinitions[index].setHexSideDrawPriority(i); } } /** * Read information on hex numbering. */ protected void readMapSheetBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Map Sheet"); int nMapSheets = ADC2Utils.readBase250Word(in); for (int i = 0; i < nMapSheets; ++i) { int x1 = ADC2Utils.readBase250Word(in); int y1 = ADC2Utils.readBase250Word(in); int x2 = ADC2Utils.readBase250Word(in); int y2 = ADC2Utils.readBase250Word(in); Rectangle r = new Rectangle(x1, y1, x2 - x1 + 1, y2 - y1 + 1); // must be exactly 9 bytes or 10 if there's a terminating null at the end String name = readNullTerminatedString(in, 10); if (name.length() < 9) in.readFully(new byte[9 - name.length()]); int style = in.readUnsignedByte(); in.readFully(new byte[2]); int nColChars = in.readUnsignedByte(); int nRowChars = in.readUnsignedByte(); if (i < nMapSheets-1) // the last one is always ignored. mapSheets.add(new MapSheet(name, r, style, nColChars, nRowChars)); } } /** * Read in information on hex sheets which define the hex numbering systems. This represents supplemental * information--some map sheet info occurs earlier in the file. Only the coordinates of the top-left corner * are read in here. */ protected void readHexNumberingBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Hex Numbering"); for (int i = 0; i < mapSheets.size()+1; ++i) { // rare case when integers are not base-250. However, they are big-endian despite being a // Windows application. int col = 0; for (int j = 0; j < 4; ++j) { col <<= 8; col += in.readUnsignedByte(); } int row = 0; for (int j = 0; j < 4; ++j) { row <<= 8; row += in.readUnsignedByte(); } if (i < mapSheets.size()) { MapSheet ms = mapSheets.get(i); ms.setTopLeftCol(col); ms.setTopLeftRow(row); } } } /** * Read and set the order of the drawn element types. */ protected void readMapItemDrawingOrderBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Map Item Drawing Order"); byte[] priority = new byte[10]; in.readFully(priority); ArrayList<MapLayer> items = new ArrayList<MapLayer>(mapElements.size()); for (int i = 0; i < mapElements.size(); ++i) { // invalid index: abort reordering and switch back to default if (priority[i] >= mapElements.size() || priority[i] < 0) return; if (i > 0) { // abort reordering and switch back to default if any indeces are repeated for (int j = 0; j < i; ++j) { if (priority[j] == priority[i]) return; } } items.add(mapElements.get(priority[i])); } // find out where it moved for (int i = 0; i < mapElements.size(); ++i) { drawingPriorities[priority[i]] = (byte) i; } // swap default order with specified order mapElements = items; } /** * Crude version information. Comes near the end of the file! Actually it's just a flag to indicate whether * the version is < 2.08. In version 2.08, the hexes are abutted slightly differently. */ protected void readVersionBlock(DataInputStream in) throws IOException{ ADC2Utils.readBlockHeader(in, "File Format Version"); int version = in.readByte(); isPreV208 = version != 0; } /** * The colour to fill before any elements are drawn. The fast-scroll flag is also read. */ protected void readTableColorBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Table Color"); /* int fastScrollFlag = */ in.readByte(); tableColor = ADC2Utils.getColorFromIndex(in.readUnsignedByte()); } /** * Optional labels that can be added to hexes. Can also include a symbol that can be added with the label. */ protected void readPlaceNameBlock(DataInputStream in) throws IOException { ADC2Utils.readBlockHeader(in, "Place Name"); int nNames = ADC2Utils.readBase250Word(in); for (int i = 0; i < nNames; ++i) { int index = ADC2Utils.readBase250Word(in); // extra hex symbol SymbolSet.SymbolData symbol = getSet().getMapBoardSymbol(ADC2Utils.readBase250Word(in)); if (symbol != null && isOnMapBoard(index)) placeSymbols.add(new HexData(index, symbol)); String text = readNullTerminatedString(in, 25); Color color = ADC2Utils.getColorFromIndex(in.readUnsignedByte()); int size = 0; for (int z = 0; z < 3; ++z) { int b = in.readUnsignedByte(); if (z == zoomLevel) size = b; } PlaceNameOrientation orientation = null; for (int z = 0; z < 3; ++z) { int o = in.readByte(); if (z == zoomLevel) { switch (o) { case 1: orientation = PlaceNameOrientation.LOWER_CENTER; break; case 2: orientation = PlaceNameOrientation.UPPER_CENTER; break; case 3: orientation = PlaceNameOrientation.LOWER_RIGHT; break; case 4: orientation = PlaceNameOrientation.UPPER_RIGHT; break; case 5: orientation = PlaceNameOrientation.UPPER_LEFT; break; case 6: orientation = PlaceNameOrientation.LOWER_LEFT; break; case 7: orientation = PlaceNameOrientation.CENTER_LEFT; break; case 8: orientation = PlaceNameOrientation.CENTER_RIGHT; break; case 9: orientation = PlaceNameOrientation.HEX_CENTER; break; } } } int font = in.readUnsignedByte(); if (!isOnMapBoard(index) || text.length() == 0 || orientation == null) continue; placeNames.add(new PlaceName(index, text, color, orientation, size, font)); } } /** * @param index Index of hex or square in row-major order starting with the upper-left-hand corner (0-based). * @return <code>true</code> if <code>index</code> is valid, <code>false</code> otherwise. */ boolean isOnMapBoard(int index) { return isOnMapBoard(index % columns, index / columns); } /** * @param x Hex or square column. * @param y Hex or square row. * @return <code>true</code> if <code>x</code> and <code>y</code> are within the bounds of the board * <code>false</code> otherwise. */ boolean isOnMapBoard(int x, int y) { return x >= 0 && x < columns && y >= 0 && y < rows; } /** * @return The Layout object corresponding to this imported map. */ protected Layout getLayout() { return layout; } /** * Returns the LineDefinition object corresponding to the given index. */ protected LineDefinition getLineDefinition(int index) { if (index < 0 | index >= lineDefinitions.length) return null; else return lineDefinitions[index]; } /** * @return Number of columns in the map. */ int getNColumns() { return columns; } /** * @return Number of rows in the map. */ int getNRows() { return rows; } /** * @return The <code>SymbolSet</code> needed by this map to render terrain and attribute elements. */ SymbolSet getSet() { return set; } @Override protected void load(File f) throws IOException { super.load(f); DataInputStream in = null; try { in = new DataInputStream(new BufferedInputStream(new FileInputStream(f))); baseName = stripExtension(f.getName()); path = f.getPath(); int header = in.readByte(); if (header != -3) throw new FileFormatException("Invalid Mapboard File Header"); // don't know what these do. in.readFully(new byte[2]); // get the symbol set String s = readWindowsFileName(in); String symbolSetFileName = forceExtension(s, "set"); set = new SymbolSet(); File setFile = action.getCaseInsensitiveFile(new File(symbolSetFileName), f, true, new ExtensionFileFilter(ADC2Utils.SET_DESCRIPTION, new String[] {ADC2Utils.SET_EXTENSION})); if (setFile == null) throw new FileNotFoundException("Unable to find symbol set file."); set.importFile(action, setFile); in.readByte(); // ignored columns = ADC2Utils.readBase250Word(in); rows = ADC2Utils.readBase250Word(in); // presumably, they're all the same size (and they're square) int hexSize = set.getMapBoardSymbolSize(); // each block read separately readHexDataBlock(in); readPlaceNameBlock(in); readHexSideBlock(in); readLineDefinitionBlock(in); readAttributeBlock(in); readMapSheetBlock(in); readHexLineBlock(in); readLineDrawPriorityBlock(in); // end of data blocks int orientation = in.read(); switch(orientation) { case 0: case 1: // vertical hex orientation or grid offset column if (set.getMapBoardSymbolShape() == SymbolSet.Shape.SQUARE) layout = new GridOffsetColumnLayout(hexSize, columns, rows); else layout = new VerticalHexLayout(hexSize, columns, rows); break; case 2: // horizontal hex orientation or grid offset row if (set.getMapBoardSymbolShape() == SymbolSet.Shape.SQUARE) layout = new GridOffsetRowLayout(hexSize, columns, rows); else layout = new HorizontalHexLayout(hexSize, columns, rows); break; default: // square grid -- no offset layout = new GridLayout(hexSize, columns, rows); } /* int saveMapPosition = */ in.readByte(); /* int mapViewingPosition = */ in.readShort(); // probably base-250 /* int mapViewingZoomLevel = */ in.readShort(); in.readByte(); // totally unknown // strangely, more blocks readTableColorBlock(in); readHexNumberingBlock(in); // TODO: default map item drawing order appears to be different for different maps. try { // optional blocks readMapBoardOverlaySymbolBlock(in); readVersionBlock(in); readMapItemDrawingOrderBlock(in); readMapItemDrawFlagBlock(in); } catch(ADC2Utils.NoMoreBlocksException e) {} in.close(); } finally { IOUtils.closeQuietly(in); } } /** * @return How many sides does each hex (6) or square (4) have? */ int getNFaces() { return getLayout().getNFaces(); } /** * @return The point corresponding to the hex centre relative to the upper-left-hand corner. */ Point getCenterOffset() { return getLayout().getOrigin(); } /** * Given a row and column for a hex, return the point corresponding to the upper left-hand pixel. */ Point coordinatesToPosition(int x, int y) { return getLayout().coordinatesToPosition(x, y, true); } /** * @param index Hex index in row-major order starting with the upper-left-hand corner (0-based). * @return upper-left-hand point of the hex or square. Returns <code>null</code> if the index is not valid. */ Point indexToPosition(int index) { return getLayout().coordinatesToPosition(index % columns, index / columns, true); } /** * @param index hex index in row-major order starting with the upper-left-hand corner (0-based). * @param nullIfOffBoard return <code>null</code> if not a valid index. If <code>false</code> will * return the point corresponding to the index if it were valid. * @return Point corresponding to the upper left hand corner of the hex or square. */ Point indexToPosition(int index, boolean nullIfOffBoard) { return getLayout().coordinatesToPosition(index % columns, index / columns, nullIfOffBoard); } /** * @param index The hex index in row major order starting with the upper-left-hand corner (0-based). * @return <code>Point</code> corresponding to the centre of that hex. */ Point indexToCenterPosition(int index) { // get upper-left-hand corner of the hex Point p = indexToPosition(index); if (p == null) return p; // shift to the centre p.translate(getLayout().getDeltaX()/2, getLayout().getDeltaY()/2); return p; } @Override public void writeToArchive() throws IOException { GameModule module = GameModule.getGameModule(); // merge layers that can and should be merged. MapLayer base = new BaseLayer(); // if there is no base map image, avoid creating a lot of layers. if (!((BaseLayer) base).hasBaseMap()) { Iterator<MapLayer> iter = mapElements.iterator(); while(iter.hasNext()) { base.overlay(iter.next()); iter.remove(); } mapElements.add(base); } else { mapElements.add(0, base); Iterator<MapLayer> iter = mapElements.iterator(); iter.next(); while(iter.hasNext()) { MapLayer next = iter.next(); if (!next.isSwitchable()) { Iterator<MapLayer> iter2 = mapElements.iterator(); MapLayer under = iter2.next(); while (under != next) { under.overlay(next); under = iter2.next(); } iter.remove(); } } mapElements.add(0, base); } for (MapLayer layer : mapElements) { layer.writeToArchive(); } // map options: log formats getMainMap().setAttribute(Map.MOVE_WITHIN_FORMAT, "$pieceName$ moving from [$previousLocation$] to [$location$]"); getMainMap().setAttribute(Map.MOVE_TO_FORMAT, "$pieceName$ moving from [$previousLocation$] to [$location$]"); getMainMap().setAttribute(Map.CREATE_FORMAT, "$pieceName$ Added to [$location$]"); // default grid AbstractConfigurable ac = getLayout().getGeometricGrid(); // TODO: set default grid numbering for maps that have no sheets (e.g., Air Assault on Crete). // ensure that we don't have a singleton null if (mapSheets.size() == 1 && mapSheets.get(0) == null) mapSheets.remove(0); // setup grids defined by ADC module Board board = getBoard(); if (mapSheets.size() > 0) { ZonedGrid zg = new ZonedGrid(); for (MapSheet ms : mapSheets) { if (ms == null) // the last one is always null break; Zone z = ms.getZone(); if (z != null) { insertComponent(z, zg); } } // add default grid insertComponent(ac, zg); // add zoned grid to board insertComponent(zg, board); } else { // add the default grid to the board insertComponent(ac, board); } /* global properties */ // for testing purposes GlobalOptions options = module.getAllDescendantComponentsOf(GlobalOptions.class).toArray(new GlobalOptions[0])[0]; options.setAttribute(GlobalOptions.AUTO_REPORT, GlobalOptions.ALWAYS); // add zoom capability if (zoomLevel > 0) { Zoomer zoom = new Zoomer(); String[] s = new String[3]; for (int i = 0; i < 3; ++i) s[i] = Double.toString(set.getZoomFactor(i)); zoom.setAttribute("zoomLevels", StringArrayConfigurer.arrayToString(s)); insertComponent(zoom, getMainMap()); } // add place name capability if (placeNames.size() > 0) { writePlaceNames(module); } // set up inventory button final Inventory inv = new Inventory(); insertComponent(inv, module); inv.setAttribute(Inventory.BUTTON_TEXT, "Search"); inv.setAttribute(Inventory.TOOLTIP, "Find place by name"); inv.setAttribute(Inventory.FILTER, "CurrentMap = Main Map && Type != Layer"); inv.setAttribute(Inventory.ICON, ""); inv.setAttribute(Inventory.GROUP_BY, "Type"); } /** * Write out place name information as non-stackable pieces which can be searched via * the piece inventory. * * @param module - Game module to write to. */ protected void writePlaceNames(GameModule module) { // write prototype PrototypesContainer container = module.getAllDescendantComponentsOf(PrototypesContainer.class).iterator().next(); PrototypeDefinition def = new PrototypeDefinition(); insertComponent(def, container); def.setConfigureName(PLACE_NAMES); GamePiece gp = new BasicPiece(); SequenceEncoder se = new SequenceEncoder(','); se.append(ADC2Utils.TYPE); gp = new Marker(Marker.ID + se.getValue(), gp); gp.setProperty(ADC2Utils.TYPE, PLACE_NAME); gp = new Immobilized(gp, Immobilized.ID + "n;V"); def.setPiece(gp); // write place names as pieces with no image. getMainMap(); final Point offset = getCenterOffset(); final HashSet<String> set = new HashSet<String>(); final Board board = getBoard(); for (PlaceName pn : placeNames) { String name = pn.getText(); Point p = pn.getPosition(); if (p == null) continue; if (set.contains(name)) continue; set.add(name); SetupStack stack = new SetupStack(); insertComponent(stack, getMainMap()); p.translate(offset.x, offset.y); String location = getMainMap().locationName(p); stack.setAttribute(SetupStack.NAME, name); stack.setAttribute(SetupStack.OWNING_BOARD, board.getConfigureName()); MapGrid mg = board.getGrid(); Zone z = null; if (mg instanceof ZonedGrid) z = ((ZonedGrid) mg).findZone(p); stack.setAttribute(SetupStack.X_POSITION, Integer.toString(p.x)); stack.setAttribute(SetupStack.Y_POSITION, Integer.toString(p.y)); if (z != null) { try { if (mg.getLocation(location) != null) { assert(mg.locationName(mg.getLocation(location)).equals(location)); stack.setAttribute(SetupStack.USE_GRID_LOCATION, true); stack.setAttribute(SetupStack.LOCATION, location); } } catch(BadCoords e) {} } BasicPiece bp = new BasicPiece(); se = new SequenceEncoder(BasicPiece.ID, ';'); se.append("").append("").append("").append(name); bp.mySetType(se.getValue()); se = new SequenceEncoder(UsePrototype.ID.replaceAll(";", ""), ';'); se.append(PLACE_NAMES); gp = new UsePrototype(se.getValue(), bp); PieceSlot ps = new PieceSlot(gp); insertComponent(ps, stack); } } /** * Does this map board use old-style hex spacing? * * @return <code>true</code> if this board is pre version 2.08, <code>false</code> if V2.08 or later. */ boolean isPreV208Layout() { return isPreV208; } /** * @return The VASSAL board object corresponding to the imported map. */ Board getBoard() { BoardPicker picker = getBoardPicker(); String boards[] = picker.getAllowableBoardNames(); assert(boards.length <= 1); Board board = null; if (boards.length == 0) { board = new Board(); insertComponent(board, picker); } else { board = picker.getBoard(boards[0]); } return board; } private ToolbarMenu getToolbarMenu() { List<ToolbarMenu> list = getMainMap().getComponentsOf(ToolbarMenu.class); ToolbarMenu menu = null; if (list.size() == 0) { menu = new ToolbarMenu(); insertComponent(menu, getMainMap()); menu.setAttribute(ToolbarMenu.BUTTON_TEXT, "View"); menu.setAttribute(ToolbarMenu.TOOLTIP, "Toggle visibility of map elements"); } else { assert(list.size() == 1); menu = list.get(0); } return menu; } /** * @return The map background colour. */ Color getTableColor() { return tableColor; } /** * @return The VASSAL BoardPicker object corresponding to this imported map. */ BoardPicker getBoardPicker() { if (boardPicker == null) boardPicker = getMainMap().getAllDescendantComponentsOf(BoardPicker.class).toArray(new BoardPicker[0])[0]; return boardPicker; } @Override public boolean isValidImportFile(File f) throws IOException { DataInputStream in = null; try { in = new DataInputStream(new FileInputStream(f)); boolean valid = in.readByte() == -3; in.close(); return valid; } finally { IOUtils.closeQuietly(in); } } }