VASSAL.tools.imports.adc2.MapBoard.java Source code

Java tutorial

Introduction

Here is the source code for VASSAL.tools.imports.adc2.MapBoard.java

Source

/*
 * 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);
        }
    }
}