/*
 * MegaMek -
 * Copyright (C) 2000,2001,2002,2003,2004,2005,2006 Ben Mazur (bmazur@sev.org)
 *
 *  This program is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU General Public License as published by the Free
 *  Software Foundation; either version 2 of the License, or (at your option)
 *  any later version.
 *
 *  This program 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 General Public License
 *  for more details.
 */

package megamek.client.ui.AWT;

import java.awt.Adjustable;
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Panel;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.PopupMenu;
import java.awt.Rectangle;
import java.awt.Scrollbar;
import java.awt.SystemColor;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.TimerTask;
import java.util.Vector;

import javax.swing.SwingUtilities;

import megamek.client.TimerSingleton;
import megamek.client.event.BoardViewEvent;
import megamek.client.event.BoardViewListener;
import megamek.client.event.MechDisplayEvent;
import megamek.client.event.MechDisplayListener;
import megamek.client.ui.IBoardView;
import megamek.client.ui.IDisplayable;
import megamek.client.ui.Messages;
import megamek.client.ui.AWT.util.ImageCache;
import megamek.client.ui.AWT.util.ImprovedAveragingScaleFilter;
import megamek.client.ui.AWT.util.KeyAlphaFilter;
import megamek.client.ui.AWT.util.PlayerColors;
import megamek.client.ui.AWT.util.StraightArrowPolygon;
import megamek.common.Aero;
import megamek.common.Building;
import megamek.common.Compute;
import megamek.common.Coords;
import megamek.common.Entity;
import megamek.common.GunEmplacement;
import megamek.common.IBoard;
import megamek.common.IEntityMovementMode;
import megamek.common.IEntityMovementType;
import megamek.common.IGame;
import megamek.common.IHex;
import megamek.common.Infantry;
import megamek.common.LosEffects;
import megamek.common.Mech;
import megamek.common.Minefield;
import megamek.common.Mounted;
import megamek.common.MovePath;
import megamek.common.MoveStep;
import megamek.common.PlanetaryConditions;
import megamek.common.Player;
import megamek.common.Protomech;
import megamek.common.SpecialHexDisplay;
import megamek.common.Tank;
import megamek.common.TargetRoll;
import megamek.common.Targetable;
import megamek.common.Terrains;
import megamek.common.UnitLocation;
import megamek.common.WeaponType;
import megamek.common.actions.ArtilleryAttackAction;
import megamek.common.actions.AttackAction;
import megamek.common.actions.ChargeAttackAction;
import megamek.common.actions.ClubAttackAction;
import megamek.common.actions.DfaAttackAction;
import megamek.common.actions.EntityAction;
import megamek.common.actions.KickAttackAction;
import megamek.common.actions.PhysicalAttackAction;
import megamek.common.actions.ProtomechPhysicalAttackAction;
import megamek.common.actions.PunchAttackAction;
import megamek.common.actions.PushAttackAction;
import megamek.common.actions.SearchlightAttackAction;
import megamek.common.actions.WeaponAttackAction;
import megamek.common.event.BoardEvent;
import megamek.common.event.BoardListener;
import megamek.common.event.GameBoardChangeEvent;
import megamek.common.event.GameBoardNewEvent;
import megamek.common.event.GameEntityChangeEvent;
import megamek.common.event.GameEntityNewEvent;
import megamek.common.event.GameEntityRemoveEvent;
import megamek.common.event.GameListener;
import megamek.common.event.GameListenerAdapter;
import megamek.common.event.GameNewActionEvent;
import megamek.common.event.GamePhaseChangeEvent;
import megamek.common.preference.IClientPreferences;
import megamek.common.preference.IPreferenceChangeListener;
import megamek.common.preference.PreferenceChangeEvent;
import megamek.common.preference.PreferenceManager;

/**
 * Displays the board; lets the user scroll around and select points on it.
 */
public class BoardView1 extends Canvas implements IBoardView, BoardListener,
        MouseListener, MouseMotionListener, KeyListener, AdjustmentListener,
        MechDisplayListener, IPreferenceChangeListener {
    private static final long serialVersionUID = -7518808415074725538L;

    private static final int TRANSPARENT = 0xFFFF00FF;

    private static final int BOARD_HEX_CLICK = 1;
    private static final int BOARD_HEX_DOUBLECLICK = 2;
    private static final int BOARD_HEX_DRAG = 3;
    private static final int BOARD_HEX_POPUP = 4;

    // the dimensions of megamek's hex images
    private static final int HEX_W = 84;
    private static final int HEX_H = 72;
    private static final int HEX_WC = HEX_W - HEX_W / 4;

    // The list of valid zoom factors. Other values cause map aliasing,
    // I can't be bothered figuring out why. - Ben
    private static final float[] ZOOM_FACTORS = { 0.30f, 0.41f, 0.50f, 0.60f,
        0.68f, 0.79f, 0.90f, 1.00f
        // 1.09f, 1.17f
    };

    // the index of zoom factor 1.00f
    private static final int BASE_ZOOM_INDEX = 7;

    // line width of the c3 network lines
    private static final int C3_LINE_WIDTH = 1;

    private static Font FONT_7 = new Font("SansSerif", Font.PLAIN, 7); //$NON-NLS-1$
    private static Font FONT_8 = new Font("SansSerif", Font.PLAIN, 8); //$NON-NLS-1$
    private static Font FONT_9 = new Font("SansSerif", Font.PLAIN, 9); //$NON-NLS-1$
    private static Font FONT_10 = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
    private static Font FONT_12 = new Font("SansSerif", Font.PLAIN, 12); //$NON-NLS-1$

    Dimension hex_size = null;
    private Font       font_note   = FONT_10;

    private Font font_hexnum = FONT_10;
    private Font font_elev = FONT_9;
    private Font font_minefield = FONT_12;

    IGame game;

    private Point mousePos = new Point();
    Rectangle view = new Rectangle();
    Point offset = new Point();
    private Dimension boardSize;

    // scrolly stuff:
    private Scrollbar vScrollbar = null;
    private Scrollbar hScrollbar = null;
    private Panel scroller;
    private boolean isScrolling = false;
    private Point scroll = new Point();
    private boolean initCtlScroll;
    private boolean ctlKeyHeld = false;
    private int previousMouseX;
    private int previousMouseY;

    // back buffer to draw to
    private Image backImage;
    Dimension backSize;
    private Graphics backGraph;

    // buffer for all the hexes you can possibly see
    private Image boardImage;
    private Rectangle boardRect;
    private Graphics boardGraph;

    // entity sprites
    private ArrayList<EntitySprite> entitySprites = new ArrayList<EntitySprite>();
    private HashMap<Integer, EntitySprite> entitySpriteIds = new HashMap<Integer, EntitySprite>();

    // sprites for the three selection cursors
    private CursorSprite cursorSprite;
    private CursorSprite highlightSprite;
    private CursorSprite selectedSprite;
    private CursorSprite firstLOSSprite;
    private CursorSprite secondLOSSprite;

    // sprite for current movement
    private ArrayList<StepSprite> pathSprites = new ArrayList<StepSprite>();

    // vector of sprites for all firing lines
    ArrayList<AttackSprite> attackSprites = new ArrayList<AttackSprite>();

    //vector of sprites for all movement paths (using vectored movement)
    private ArrayList<MovementSprite> movementSprites = new ArrayList<MovementSprite>();

    // vector of sprites for C3 network lines
    private ArrayList<C3Sprite> C3Sprites = new ArrayList<C3Sprite>();

    // tooltip stuff
    private Window tipWindow;
    private boolean isTipPossible = false;
    private long lastIdle;

    TilesetManager tileManager = null;

    // polygons for a few things
    Polygon hexPoly;
    Polygon[] facingPolys;
    Polygon[] movementPolys;

    // the player who owns this BoardView's client
    private Player localPlayer = null;

    // should we mark deployment hexes for a player?
    private Player m_plDeployer = null;

    // should be able to turn it off(board editor)
    private boolean useLOSTool = true;

    // Initial scale factor for sprites and map
    public int zoomIndex;
    float scale;
    private ImageCache<Image, Image> scaledImageCache = new ImageCache<Image, Image>();

    // Displayables (Chat box, etc.)
    ArrayList<IDisplayable> displayables = new ArrayList<IDisplayable>();

    // Move units step by step
    private ArrayList<MovingUnit> movingUnits = new ArrayList<MovingUnit>();
    private long moveWait = 0;

    // moving entity sprites
    private ArrayList<MovingEntitySprite> movingEntitySprites = new ArrayList<MovingEntitySprite>();
    private HashMap<Integer, MovingEntitySprite> movingEntitySpriteIds = new HashMap<Integer, MovingEntitySprite>();
    private ArrayList<GhostEntitySprite> ghostEntitySprites = new ArrayList<GhostEntitySprite>();
    protected transient ArrayList<BoardViewListener> boardListeners = new ArrayList<BoardViewListener>();

    // wreck sprites
    private ArrayList<WreckSprite> wreckSprites = new ArrayList<WreckSprite>();

    private Coords rulerStart; // added by kenn
    private Coords rulerEnd; // added by kenn
    private Color rulerStartColor; // added by kenn
    private Color rulerEndColor; // added by kenn

    // Position of the mouse before right mouse button was pressed. Used to have
    // an anchor for scrolling
    private Point oldMousePosition = null;

    // Indicate that a scrolling took place, so no popup should be drawn on
    // right mouse button release
    private boolean scrolled = false;

    private Coords lastCursor;
    private Coords highlighted;
    private Coords selected;
    private Coords firstLOS;

    // selected entity and weapon for artillery display
    private Entity selectedEntity = null;
    private Mounted selectedWeapon = null;

    // hexes with ECM effect
    private HashMap<Coords, Integer> ecmHexes = null;

    private boolean dirtyBoard = true;

    /**
     * Construct a new board view for the specified game
     */
    public BoardView1(IGame game) throws IOException {
        this.game = game;
        tileManager = new TilesetManager(this);

        game.addGameListener(gameListener);
        game.getBoard().addBoardListener(this);
        scheduleRedrawTimer();

        addKeyListener(this);
        addMouseListener(this);
        addMouseMotionListener(this);
        addMouseWheelListener(new MouseWheelListener() {
            public void mouseWheelMoved(MouseWheelEvent we) {
                if (GUIPreferences.getInstance().getMouseWheelZoom()) {
                    if (we.getWheelRotation() > 0) {
                        zoomIn();
                    } else {
                        zoomOut();
                    }
                }
            }
        });

        zoomIndex = GUIPreferences.getInstance().getMapZoomIndex();
        checkZoomIndex();
        scale = ZOOM_FACTORS[zoomIndex];

        updateFontSizes();
        updateBoardSize();

        // tooltip
        tipWindow = new Window(new Frame());

        hex_size = new Dimension((int) (HEX_W * scale), (int) (HEX_H * scale));

        initPolys();

        cursorSprite = new CursorSprite(Color.cyan);
        highlightSprite = new CursorSprite(Color.white);
        selectedSprite = new CursorSprite(Color.blue);
        firstLOSSprite = new CursorSprite(Color.red);
        secondLOSSprite = new CursorSprite(Color.red);

        PreferenceManager.getClientPreferences().addPreferenceChangeListener(
                this);

        SpecialHexDisplay.Type.ARTILLERY_HIT.init(getToolkit());
        SpecialHexDisplay.Type.ARTILLERY_INCOMING.init(getToolkit());
        SpecialHexDisplay.Type.ARTILLERY_TARGET.init(getToolkit());
        SpecialHexDisplay.Type.ARTILLERY_ADJUSTED.init(getToolkit());
        SpecialHexDisplay.Type.ARTILLERY_AUTOHIT.init(getToolkit());
    }

    protected final RedrawWorker redrawWorker = new RedrawWorker();

    protected void scheduleRedrawTimer() {
        final TimerTask redraw = new TimerTask() {
            @Override
            public void run() {
                try {
                    SwingUtilities.invokeAndWait(redrawWorker);
                } catch (Exception ie) {
                }
            }
        };
        TimerSingleton.getInstance().schedule(redraw, 20, 20);
    }

    protected void scheduleRedraw() {
        try {
            SwingUtilities.invokeLater(redrawWorker);
        } catch (Exception ie) {
        }
    }

    public void preferenceChange(PreferenceChangeEvent e) {
        if (e.getName().equals(IClientPreferences.MAP_TILESET)) {
            updateBoard();
        }
    }

    /**
     * Adds the specified board listener to receive board events from this
     * board.
     *
     * @param listener the board listener.
     */
    public void addBoardViewListener(BoardViewListener listener) {
        if (!boardListeners.contains(listener)) {
            boardListeners.add(listener);
        }
    }

    /**
     * Removes the specified board listener.
     *
     * @param listener the board listener.
     */
    public void removeBoardViewListener(BoardViewListener listener) {
        boardListeners.remove(listener);
    }

    /**
     * Notifies attached board listeners of the event.
     *
     * @param event the board event.
     */
    public void processBoardViewEvent(BoardViewEvent event) {
        if (boardListeners == null) {
            return;
        }
        for (BoardViewListener l : boardListeners) {
            switch (event.getType()) {
            case BoardViewEvent.BOARD_HEX_CLICKED:
            case BoardViewEvent.BOARD_HEX_DOUBLECLICKED:
            case BoardViewEvent.BOARD_HEX_DRAGGED:
            case BoardViewEvent.BOARD_HEX_POPUP:
                l.hexMoused(event);
                break;
            case BoardViewEvent.BOARD_HEX_CURSOR:
                l.hexCursor(event);
                break;
            case BoardViewEvent.BOARD_HEX_HIGHLIGHTED:
                l.boardHexHighlighted(event);
                break;
            case BoardViewEvent.BOARD_HEX_SELECTED:
                l.hexSelected(event);
                break;
            case BoardViewEvent.BOARD_FIRST_LOS_HEX:
                l.firstLOSHex(event);
                break;
            case BoardViewEvent.BOARD_SECOND_LOS_HEX:
                l.secondLOSHex(event, getFirstLOS());
                break;
            case BoardViewEvent.FINISHED_MOVING_UNITS:
                l.finishedMovingUnits(event);
                break;
            case BoardViewEvent.SELECT_UNIT:
                l.unitSelected(event);
                break;
            }
        }
    }

    void addMovingUnit(Entity entity, Vector<UnitLocation> movePath) {
        if (!movePath.isEmpty()) {
            MovingUnit m = new MovingUnit(entity, movePath);
            movingUnits.add(m);

            GhostEntitySprite ghostSprite = new GhostEntitySprite(entity);
            ghostEntitySprites.add(ghostSprite);

            // Center on the starting hex of the moving unit.
            UnitLocation loc = (movePath.elementAt(0));
            centerOnHex(loc.getCoords());
        }
    }

    public void addDisplayable(IDisplayable disp) {
        displayables.add(disp);
    }

    public void removeDisplayable(IDisplayable disp) {
        displayables.remove(disp);
    }

    /**
     * Update ourself when a scroll bar is adjusted.
     *
     * @param event - the <code>AdjustmentEvent</code> that caused this call.
     */
    public void adjustmentValueChanged(AdjustmentEvent event) {
        Point oldPt = scroll;
        Point newPt = new Point(oldPt.x, oldPt.y);
        if (event.getAdjustable().getOrientation() == Adjustable.VERTICAL) {
            newPt.y = event.getValue();
        } else {
            newPt.x = event.getValue();
        }
        scroll.setLocation(newPt);
        this.repaint();
    }

    @Override
    public void paint(Graphics g) {
        update(g);
    }

    /**
     * Draw the screen!
     */
    @Override
    public synchronized void update(Graphics g) {
        // Limit our size to the viewport of the scroll pane.
        final Dimension size = getSize();
        // final long startTime = System.currentTimeMillis(); // commentme

        // Make sure our scrollbars have the right sizes.
        // N.B. A buggy Sun implementation makes me to do this here instead
        // of updateBoardSize() (which is where *I* think it belongs).
        if (null != vScrollbar) {
            vScrollbar.setVisibleAmount(size.height);
            vScrollbar.setBlockIncrement(size.height);
            vScrollbar.setUnitIncrement((int) (scale * HEX_H / 2.0));
            vScrollbar.setMaximum(boardSize.height);
        }
        if (null != hScrollbar) {
            hScrollbar.setVisibleAmount(size.width);
            hScrollbar.setBlockIncrement(size.width);
            hScrollbar.setUnitIncrement((int) (scale * HEX_W / 2.0));
            hScrollbar.setMaximum(boardSize.width);
        }

        // update view, offset
        view.setLocation(scroll);
        view.setSize(getOptimalView(size));
        offset.setLocation(getOptimalOffset(size));

        if (!isTileImagesLoaded()) {
            g.drawString(
                    Messages.getString("BoardView1.loadingImages"), 20, 50); //$NON-NLS-1$
            if (!tileManager.isStarted()) {
                System.out.println("boardview1: loading images for board"); //$NON-NLS-1$
                tileManager.loadNeededImages(game);
            }
            return;
        }

        // make sure back buffer is valid
        if ((backGraph == null) || !view.getSize().equals(backSize)) {
            // make new back buffer
            backSize = view.getSize();
            backImage = createImage(backSize.width, backSize.height);
            backGraph = backImage.getGraphics();
        }

        // make sure board rectangle contains our current view rectangle
        if ((boardImage == null) || !boardRect.union(view).equals(boardRect)
                || dirtyBoard) {
            updateBoardImage();
        }

        // draw onto the back buffer:

        // redraw all the specials. This is an inneficient hack :/
        for(Coords c : game.getBoard().getSpecialHexDisplayTable().keySet()) {
            drawHex(c);
        }

        // draw the board
        backGraph.drawImage(boardImage, 0, 0, this);

        // draw wrecks
        if (GUIPreferences.getInstance().getShowWrecks()) {
            drawSprites(wreckSprites);
        }

        // Minefield signs all over the place!
        drawMinefields();

        // draw highlight border
        drawSprite(highlightSprite);

        // draw cursors
        drawSprite(cursorSprite);
        drawSprite(selectedSprite);
        drawSprite(firstLOSSprite);
        drawSprite(secondLOSSprite);

        // draw deployment indicators
        if (m_plDeployer != null) {
            drawDeployment();
        }

        // draw C3 links
        drawSprites(C3Sprites);

        // draw onscreen entities
        drawSprites(entitySprites);

        // draw moving onscreen entities
        drawSprites(movingEntitySprites);

        // draw ghost onscreen entities
        drawSprites(ghostEntitySprites);

        // draw onscreen attacks
        drawSprites(attackSprites);

        //draw movement vectors.
        if(game.useVectorMove() && (game.getPhase() == IGame.Phase.PHASE_MOVEMENT)) {
            drawSprites(movementSprites);
        }

        // draw movement, if valid
        drawSprites(pathSprites);

        // draw the ruler line
        if (rulerStart != null) {
            Point start = getCentreHexLocation(rulerStart);
            if (rulerEnd != null) {
                Point end = getCentreHexLocation(rulerEnd);
                backGraph.setColor(Color.yellow);
                backGraph.drawLine(start.x - boardRect.x,
                        start.y - boardRect.y, end.x - boardRect.x, end.y
                        - boardRect.y);

                backGraph.setColor(rulerEndColor);
                backGraph.fillRect(end.x - boardRect.x - 1, end.y - boardRect.y
                        - 1, 2, 2);
            }

            backGraph.setColor(rulerStartColor);
            backGraph.fillRect(start.x - boardRect.x - 1, start.y - boardRect.y
                    - 1, 2, 2);
        }

        // draw all the "displayables"
        for (int i = 0; i < displayables.size(); i++) {
            IDisplayable disp = displayables.get(i);
            disp.draw(backGraph, new Point(backSize.width, backSize.height), backSize);
        }

        // draw the back buffer onto the screen
        // first clear the entire view if the map has been zoomed
        if (scale < 1.00f) {
            Image tmpImage = createImage(size.width, size.height);
            Graphics tmpGraphics = tmpImage.getGraphics();
            tmpGraphics.drawImage(backImage, offset.x, offset.y, this);
            g.drawImage(tmpImage, 0, 0, this);
            tmpGraphics.dispose();
        } else {
            g.drawImage(backImage, offset.x, offset.y, this);
        }
    }

    /**
     * Updates the boardSize variable with the proper values for this board.
     */
    private void updateBoardSize() {
        int width = game.getBoard().getWidth() * (int) (HEX_WC * scale)
                + (int) (HEX_W / 4 * scale);
        int height = game.getBoard().getHeight() * (int) (HEX_H * scale)
                + (int) (HEX_H / 2 * scale);
        boardSize = new Dimension(width, height);
    }

    /**
     * Think up the size of the view rectangle based on the size of the
     * component and the size of board
     */
    private Dimension getOptimalView(Dimension size) {
        return new Dimension(Math.min(size.width, boardSize.width), Math.min(
                size.height, boardSize.height));
    }

    /**
     * Where should the offset be for this screen size?
     */
    private Point getOptimalOffset(Dimension size) {
        int ox = 0;
        int oy = 0;
        if (size.width > boardSize.width) {
            ox = (size.width - boardSize.width) / 2;
        }
        if (size.height > boardSize.height) {
            oy = (size.height - boardSize.height) / 2;
        }
        return new Point(ox, oy);
    }

    /**
     * Repaint the bounds of a sprite, offset by view
     */
    private void repaintBounds(Rectangle bounds) {
        if (view != null) {
            repaint(bounds.x - view.x + offset.x, bounds.y - view.y + offset.y,
                    bounds.width, bounds.height);
        }
    }

    /**
     * Looks through a vector of buffered images and draws them if they're
     * onscreen.
     */
    private synchronized void drawSprites(
            ArrayList<? extends Sprite> spriteArrayList) {
        for (Sprite sprite : spriteArrayList) {
            drawSprite(sprite);
        }
    }

    /**
     * Draws a sprite, if it is in the current view
     */
    private final void drawSprite(Sprite sprite) {
        if (view.intersects(sprite.getBounds()) && !sprite.hidden) {
            final int drawX = sprite.getBounds().x - view.x;
            final int drawY = sprite.getBounds().y - view.y;
            if (!sprite.isReady()) {
                sprite.prepare();
            }
            sprite.drawOnto(backGraph, drawX, drawY, this);
        }
    }

    /**
     * Manages a cache of scaled images.
     */
    Image getScaledImage(Image base) {
        if (base == null) {
            return null;
        }
        if (zoomIndex == BASE_ZOOM_INDEX) {
            return base;
        }

        Image scaled = scaledImageCache.get(base);
        if (scaled == null) {
            MediaTracker tracker = new MediaTracker(this);
            if ((base.getWidth(null) == -1) || (base.getHeight(null) == -1)) {
                tracker.addImage(base, 0);
                try {
                    tracker.waitForID(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tracker.removeImage(base);
            }
            int width = (int) (base.getWidth(null) * scale);
            int height = (int) (base.getHeight(null) * scale);

            //TODO: insert a check that width and height are > 0.

            scaled = scale(base, width, height);
            tracker.addImage(scaled, 1);
            // Wait for image to load
            try {
                tracker.waitForID(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tracker.removeImage(scaled);

            scaledImageCache.put(base, scaled);
        }
        return scaled;
    }

    /**
     * The actual scaling code.
     */
    private Image scale(Image img, int width, int height) {
        ImageFilter filter;
        filter = new ImprovedAveragingScaleFilter(img.getWidth(null), img
                .getHeight(null), width, height);

        ImageProducer prod;
        prod = new FilteredImageSource(img.getSource(), filter);
        return Toolkit.getDefaultToolkit().createImage(prod);
    }

    /**
     * Draw an outline around legal deployment hexes
     */
    private void drawDeployment() {
        // only update visible hexes
        int drawX = view.x / (int) (HEX_WC * scale) - 1;
        int drawY = view.y / (int) (HEX_H * scale) - 1;

        int drawWidth = view.width / (int) (HEX_WC * scale) + 3;
        int drawHeight = view.height / (int) (HEX_H * scale) + 3;
        IBoard board = game.getBoard();
        // loop through the hexes
        for (int i = 0; i < drawHeight; i++) {
            for (int j = 0; j < drawWidth; j++) {
                Coords c = new Coords(j + drawX, i + drawY);
                Point p = getHexLocation(c);
                p.translate(-(view.x), -(view.y));
                if (board.isLegalDeployment(c, m_plDeployer)) {
                    backGraph.setColor(Color.yellow);
                    int[] xcoords = { p.x + (int) (21 * scale),
                            p.x + (int) (62 * scale), p.x + (int) (83 * scale),
                            p.x + (int) (83 * scale), p.x + (int) (62 * scale),
                            p.x + (int) (21 * scale), p.x, p.x };
                    int[] ycoords = { p.y, p.y, p.y + (int) (35 * scale),
                            p.y + (int) (36 * scale), p.y + (int) (71 * scale),
                            p.y + (int) (71 * scale), p.y + (int) (36 * scale),
                            p.y + (int) (35 * scale) };
                    backGraph.drawPolygon(xcoords, ycoords, 8);
                }
            }
        }
    }

    /**
     * returns the weapon selected in the mech display, or null if none selected
     * or it is not artillery or null if the selected entity is not owned
     */
    private Mounted getSelectedArtilleryWeapon() {
        if ((selectedEntity == null) || (selectedWeapon == null)) {
            return null;
        }

        if (!selectedEntity.getOwner().equals(localPlayer)) {
            return null; // Not my business to see this
        }

        if (selectedEntity.getEquipmentNum(selectedWeapon) == -1) {
            return null; // inconsistent state - weapon not on entity
        }

        if (!((selectedWeapon.getType() instanceof WeaponType) && selectedWeapon
                .getType().hasFlag(WeaponType.F_ARTILLERY))) {
            return null; // not artillery
        }

        // otherwise, a weapon is selected, and it is artillery
        return selectedWeapon;
    }

    private ArrayList<ArtilleryAttackAction> getArtilleryAttacksAtLocation(
            Coords c) {
        ArrayList<ArtilleryAttackAction> v = new ArrayList<ArtilleryAttackAction>();
        for (Enumeration<ArtilleryAttackAction> attacks = game
                .getArtilleryAttacks(); attacks.hasMoreElements();) {
            ArtilleryAttackAction a = attacks.nextElement();
            if (a.getTarget(game).getPosition().equals(c)) {
                v.add(a);
            }
        }
        return v;
    }

    /**
     * Writes "MINEFIELD" in minefield hexes...
     */
    private void drawMinefields() {
        // only update visible hexes
        int drawX = view.x / (int) (HEX_WC * scale) - 1;
        int drawY = view.y / (int) (HEX_H * scale) - 1;

        int drawWidth = view.width / (int) (HEX_WC * scale) + 3;
        int drawHeight = view.height / (int) (HEX_H * scale) + 3;

        IBoard board = game.getBoard();
        // loop through the hexes
        for (int i = 0; i < drawHeight; i++) {
            for (int j = 0; j < drawWidth; j++) {
                Coords c = new Coords(j + drawX, i + drawY);
                Point p = getHexLocation(c);
                p.translate(-(view.x), -(view.y));

                if (!board.contains(c)) {
                    continue;
                }
                if (!game.containsMinefield(c)) {
                    continue;
                }

                Minefield mf = game.getMinefields(c).elementAt(0);

                Image tmpImage = getScaledImage(tileManager.getMinefieldSign());
                backGraph.drawImage(tmpImage, p.x + (int) (13 * scale), p.y
                        + (int) (13 * scale), this);

                backGraph.setColor(Color.black);
                int nbrMfs = game.getNbrMinefields(c);
                if (nbrMfs > 1) {
                    drawCenteredString(
                            Messages.getString("BoardView1.Multiple"), //$NON-NLS-1$
                            p.x, p.y + (int) (51 * scale), font_minefield,
                            backGraph);
                } else if (nbrMfs == 1) {
                    switch (mf.getType()) {
                    case (Minefield.TYPE_CONVENTIONAL):
                        drawCenteredString(
                                Messages
                                .getString("BoardView1.Conventional") + mf.getDensity() + ")", //$NON-NLS-1$
                                p.x, p.y + (int) (51 * scale),
                                font_minefield, backGraph);
                    if(mf.isSeaBased()) {
                        drawCenteredString(
                                "Depth: " + mf.getDepth() + "", //$NON-NLS-1$ //$NON-NLS-2$
                                p.x, p.y + (int) (60 * scale),
                                font_minefield, backGraph);
                    }
                    break;
                    case (Minefield.TYPE_INFERNO):
                        drawCenteredString(
                                Messages
                                .getString("BoardView1.Inferno") + mf.getDensity() + ")", //$NON-NLS-1$ //$NON-NLS-2$
                                p.x, p.y + (int) (51 * scale),
                                font_minefield, backGraph);
                    break;
                    case (Minefield.TYPE_ACTIVE):
                        drawCenteredString(
                                Messages
                                .getString("BoardView1.Active") + mf.getDensity() + ")", //$NON-NLS-1$ //$NON-NLS-2$
                                p.x, p.y + (int) (51 * scale),
                                font_minefield, backGraph);
                    break;
                    case (Minefield.TYPE_COMMAND_DETONATED):
                        drawCenteredString(
                                Messages.getString("BoardView1.Command") + mf.getDensity() + ")", //$NON-NLS-1$
                                p.x, p.y + (int) (51 * scale),
                                font_minefield, backGraph);
                    break;
                    case (Minefield.TYPE_VIBRABOMB):
                        drawCenteredString(
                                Messages.getString("BoardView1.Vibrabomb") + mf.getDensity() + ")", //$NON-NLS-1$
                                p.x, p.y + (int) (51 * scale),
                                font_minefield, backGraph);
                    if (mf.getPlayerId() == localPlayer.getId()) {
                        drawCenteredString(
                                "(" + mf.getSetting() + ")", //$NON-NLS-1$ //$NON-NLS-2$
                                p.x, p.y + (int) (60 * scale),
                                font_minefield, backGraph);
                    }
                    break;
                    }
                }
            }
        }
    }

    private void drawCenteredString(String string, int x, int y, Font font,
            Graphics graph) {
        FontMetrics currentMetrics = getFontMetrics(font);
        int stringWidth = currentMetrics.stringWidth(string);

        x += ((hex_size.width - stringWidth) / 2);

        graph.setFont(font);
        graph.drawString(string, x, y);
    }

    /**
     * Updates the board buffer to contain all the hexes needed by the view.
     */
    private void updateBoardImage() {
        // check to make sure image is big enough
        if ((boardGraph == null) || (view.width > boardRect.width)
                || (view.height > boardRect.height)) {
            /*
             * Ok, some history here. Before the zoom patch, the boardImage was
             * created with the same size as the view. After the zoom patch, the
             * boardImage was created with the same size as the entire board
             * (all maps). This change ate up a hideous amount of memory (eg: in
             * a 3x3 map set test with one mech, memory usage went from about
             * 15MB to 60MB). I have now changed it back to the old way, and the
             * zoom feature *seems* to still work. Why the zoom author made the
             * change, I cannot say.
             */
            boardImage = createImage(view.width, view.height);
            // boardImage = createImage(boardSize.width, boardSize.height);
            /* ----- */

            if (boardGraph != null) {
                boardGraph.dispose();
            }
            boardGraph = boardImage.getGraphics();

            // Handle resizes correctly.
            checkScrollBounds();
            boardRect = new Rectangle(view);
            System.out
                    .println("boardview1: made a new board buffer " + boardRect); //$NON-NLS-1$
            drawHexes(view);
            dirtyBoard = false;
        }
        if (!boardRect.union(view).equals(boardRect) || dirtyBoard) {
            moveBoardImage();
        }
    }

    /**
     * This method creates an image the size of the entire board (all
     * mapsheets), draws the hexes onto it, and returns that image.
     */
    public Image getEntireBoardImage() {
        Image entireBoard = createImage(boardSize.width, boardSize.height);
        Graphics temp = boardImage.getGraphics();
        boardGraph = entireBoard.getGraphics();
        drawHexes(new Rectangle(boardSize));
        boardGraph.dispose();
        boardGraph = temp;
        return entireBoard;
    }

    /**
     * Moves the board view to another area.
     */
    private void moveBoardImage() {
        // salvage the old

        boardGraph.setClip(0, 0, boardRect.width, boardRect.height);

        boardGraph.copyArea(0, 0, boardRect.width, boardRect.height,
                boardRect.x - view.x, boardRect.y - view.y);

        // what's left to paint?
        int midX = Math.max(view.x, boardRect.x);
        int midWidth = view.width - Math.abs(view.x - boardRect.x);
        Rectangle unLeft = new Rectangle(view.x, view.y, boardRect.x - view.x,
                view.height);
        Rectangle unRight = new Rectangle(boardRect.x + boardRect.width,
                view.y, view.x - boardRect.x, view.height);
        Rectangle unTop = new Rectangle(midX, view.y, midWidth, boardRect.y
                - view.y);
        Rectangle unBottom = new Rectangle(midX,
                boardRect.y + boardRect.height, midWidth, view.y - boardRect.y);

        // update boardRect
        boardRect = new Rectangle(view);

        if (dirtyBoard) {
            drawHexes(view);
            dirtyBoard = false;
        } else {
            // paint needed areas
            if (unLeft.width > 0) {
                drawHexes(unLeft);
            } else if (unRight.width > 0) {
                drawHexes(unRight);
            }
            if (unTop.height > 0) {
                drawHexes(unTop);
            } else if (unBottom.height > 0) {
                drawHexes(unBottom);
            }
        }
    }

    /**
     * Redraws all hexes in the specified rectangle
     */
    private void drawHexes(Rectangle rect) {

        // rect is the view
        int drawX = (int) (rect.x / (HEX_WC * scale)) - 1;
        int drawY = (int) (rect.y / (HEX_H * scale)) - 1;

        int drawWidth = (int) (rect.width / (HEX_WC * scale)) + 3;
        int drawHeight = (int) (rect.height / (HEX_H * scale)) + 3;

        // only draw what we came to draw
        boardGraph.setClip(rect.x - boardRect.x, rect.y - boardRect.y,
                rect.width, rect.height);

        // clear, if we need to
        if (rect.x < (21 * scale)) {
            boardGraph.clearRect(rect.x - boardRect.x, rect.y - boardRect.y,
                    (int) (21 * scale) - rect.x, rect.height);
        }
        if (rect.y < (36 * scale)) {
            boardGraph.clearRect(rect.x - boardRect.x, rect.y - boardRect.y,
                    rect.width, (int) (36 * scale) - rect.y);
        }
        if (rect.x > boardSize.width - view.width - (21 * scale)) {
            boardGraph.clearRect(boardRect.width - (int) (21 * scale), rect.y
                    - boardRect.y, (int) (21 * scale), rect.height);
        }
        if (rect.y > boardSize.height - view.height - (int) (36 * scale)) {
            boardGraph.clearRect(rect.x - boardRect.x, boardRect.height
                    - (int) (36 * scale), rect.width, (int) (36 * scale));
        }

        // draw some hexes
        for (int i = 0; i < drawHeight; i++) {
            for (int j = 0; j < drawWidth; j++) {
                drawHex(new Coords(j + drawX, i + drawY));
            }
        }
    }

    /**
     * Redraws a hex and all the hexes immediately around it. Used when the hex
     * is on the screen, as opposed to when it is scrolling onto the screen, so
     * it resets the clipping rectangle before drawing.
     */
    private void redrawAround(Coords c) {
        if(c != null) {
            boardGraph.setClip(0, 0, boardRect.width, boardRect.height);
            drawHex(c);
            drawHex(c.translated(0));
            drawHex(c.translated(1));
            drawHex(c.translated(2));
            drawHex(c.translated(3));
            drawHex(c.translated(4));
            drawHex(c.translated(5));
        }
    }

    /**
     * Draws a hex onto the board buffer. This assumes that boardRect is
     * current, and does not check if the hex is visible.
     */
    private void drawHex(Coords c) {
        if (!game.getBoard().contains(c)) {
            return;
        }

        final IHex hex = game.getBoard().getHex(c);
        final Point hexLoc = getHexLocation(c);

        int level = hex.getElevation();
        int depth = hex.depth();
        int height = Math.max(hex.terrainLevel(Terrains.BLDG_ELEV), hex
                .terrainLevel(Terrains.BRIDGE_ELEV));
        height = Math.max(height, hex.terrainLevel(Terrains.INDUSTRIAL));

        // offset drawing point

        int drawX = hexLoc.x - boardRect.x;
        int drawY = hexLoc.y - boardRect.y;

        // draw picture
        Image baseImage = tileManager.baseFor(hex);
        Image scaledImage = getScaledImage(baseImage);

        boardGraph.drawImage(scaledImage, drawX, drawY, this);

        if (tileManager.supersFor(hex) != null) {
            for (Image image : tileManager.supersFor(hex)) {
                scaledImage = getScaledImage(image);
                boardGraph.drawImage(scaledImage, drawX, drawY, this);
            }
        }

        if (ecmHexes != null) {
            Integer tint = ecmHexes.get(c);
            if (tint != null) {
                scaledImage = getScaledImage(tileManager.getEcmShade(tint
                        .intValue()));
                boardGraph.drawImage(scaledImage, drawX, drawY, this);
            }
        }

        if (GUIPreferences.getInstance().getBoolean(
                GUIPreferences.ADVANCED_DARKEN_MAP_AT_NIGHT)
                && (game.getPlanetaryConditions().getLight() > PlanetaryConditions.L_DAY)
                && !game.isPositionIlluminated(c)) {
            scaledImage = getScaledImage(tileManager.getNightFog());
            boardGraph.drawImage(scaledImage, drawX, drawY, this);
        }
        boardGraph.setColor(GUIPreferences.getInstance().getMapTextColor());

        //draw special stuff for the hex
        final Collection<SpecialHexDisplay> shdList = game.getBoard().getSpecialHexDisplay(c);
        try {
            if(shdList != null) {
                for(SpecialHexDisplay shd : shdList)
                {
                    if (shd.drawNow(game.getPhase(), game.getRoundCount())) {
                        scaledImage = getScaledImage(shd.getType().getDefaultImage());
                        if(scaledImage != null) {
                            boardGraph.drawImage(scaledImage, drawX, drawY, this);
                        }
                    }
                }
            }
        } catch (IllegalArgumentException e) {
            System.err.println("Illegal argument exception, probably can't load file.");
            e.printStackTrace();
            drawCenteredString(
                    "Loading Error",
                    drawX,
                    drawY + (int)(50*scale),
                    font_note,
                    boardGraph);
            return;
        }

        // draw hex number
        if (scale >= 0.5) {
            drawCenteredString(c.getBoardNum(), drawX, drawY
                    + (int) (12 * scale), font_hexnum, boardGraph);
        }
        // draw terrain level / water depth / building height
        if (zoomIndex > 3) {
            int ypos = 70;
            if(null != shdList) {
                Color oldColor = boardGraph.getColor();
                boardGraph.setColor(Color.RED);
                for(SpecialHexDisplay shd : shdList)
                {
                    if(SpecialHexDisplay.Type.PLAYER_NOTE == shd.getType()) {
                        drawCenteredString(
                                shd.getInfo(),
                                drawX,
                                drawY + (int)(ypos*scale),
                                font_note,
                                boardGraph);
                        ypos -= 10;
                    }
                }

                boardGraph.setColor(oldColor);
            }
            if (level != 0) {
                drawCenteredString(
                        Messages.getString("BoardView1.LEVEL") + level, //$NON-NLS-1$
                        drawX, drawY + (int) (ypos * scale), font_elev,
                        boardGraph);
                ypos -= 10;
            }
            if (depth != 0) {
                drawCenteredString(
                        Messages.getString("BoardView1.DEPTH") + depth, //$NON-NLS-1$
                        drawX, drawY + (int) (ypos * scale), font_elev,
                        boardGraph);
                ypos -= 10;
            }
            if (height > 0) {
                boardGraph.setColor(GUIPreferences.getInstance().getColor(
                "AdvancedBuildingTextColor"));
                drawCenteredString(
                        Messages.getString("BoardView1.HEIGHT") + height, //$NON-NLS-1$
                        drawX, drawY + (int) (ypos * scale), font_elev,
                        boardGraph);
                ypos -= 10;
            }
        }

        // draw elevation borders
        boardGraph.setColor(Color.black);
        if (drawElevationLine(c, 0)) {
            boardGraph.drawLine(drawX + (int) (21 * scale), drawY, drawX
                    + (int) (62 * scale), drawY);
        }
        if (drawElevationLine(c, 1)) {
            boardGraph.drawLine(drawX + (int) (62 * scale), drawY, drawX
                    + (int) (83 * scale), drawY + (int) (35 * scale));
        }
        if (drawElevationLine(c, 2)) {
            boardGraph.drawLine(drawX + (int) (83 * scale), drawY
                    + (int) (36 * scale), drawX + (int) (62 * scale), drawY
                    + (int) (71 * scale));
        }
        if (drawElevationLine(c, 3)) {
            boardGraph.drawLine(drawX + (int) (62 * scale), drawY
                    + (int) (71 * scale), drawX + (int) (21 * scale), drawY
                    + (int) (71 * scale));
        }
        if (drawElevationLine(c, 4)) {
            boardGraph.drawLine(drawX + (int) (21 * scale), drawY
                    + (int) (71 * scale), drawX, drawY + (int) (36 * scale));
        }
        if (drawElevationLine(c, 5)) {
            boardGraph.drawLine(drawX, drawY + (int) (35 * scale), drawX
                    + (int) (21 * scale), drawY);
        }

        // draw mapsheet borders
        if (GUIPreferences.getInstance().getShowMapsheets()) {
            boardGraph.setColor(GUIPreferences.getInstance().getColor(
                    GUIPreferences.ADVANCED_MAPSHEET_COLOR));
            if (c.x % 16 == 0) {
                // left edge of sheet (edge 4 & 5)
                boardGraph
                .drawLine(drawX + (int) (21 * scale), drawY
                        + (int) (71 * scale), drawX, drawY
                        + (int) (36 * scale));
                boardGraph.drawLine(drawX, drawY + (int) (35 * scale), drawX
                        + (int) (21 * scale), drawY);
            } else if (c.x % 16 == 15) {
                // right edge of sheet (edge 1 & 2)
                boardGraph.drawLine(drawX + (int) (62 * scale), drawY, drawX
                        + (int) (83 * scale), drawY + (int) (35 * scale));
                boardGraph.drawLine(drawX + (int) (83 * scale), drawY
                        + (int) (36 * scale), drawX + (int) (62 * scale), drawY
                        + (int) (71 * scale));
            }
            if (c.y % 17 == 0) {
                // top edge of sheet (edge 0 and possible 1 & 5)
                boardGraph.drawLine(drawX + (int) (21 * scale), drawY, drawX
                        + (int) (62 * scale), drawY);
                if (c.x % 2 == 0) {
                    boardGraph.drawLine(drawX + (int) (62 * scale), drawY,
                            drawX + (int) (83 * scale), drawY
                            + (int) (35 * scale));
                    boardGraph.drawLine(drawX, drawY + (int) (35 * scale),
                            drawX + (int) (21 * scale), drawY);
                }
            } else if (c.y % 17 == 16) {
                // bottom edge of sheet (edge 3 and possible 2 & 4)
                boardGraph.drawLine(drawX + (int) (62 * scale), drawY
                        + (int) (71 * scale), drawX + (int) (21 * scale), drawY
                        + (int) (71 * scale));
                if (c.x % 2 == 1) {
                    boardGraph.drawLine(drawX + (int) (83 * scale), drawY
                            + (int) (36 * scale), drawX + (int) (62 * scale),
                            drawY + (int) (71 * scale));
                    boardGraph.drawLine(drawX + (int) (21 * scale), drawY
                            + (int) (71 * scale), drawX, drawY
                            + (int) (36 * scale));
                }
            }
            boardGraph.setColor(Color.black);
        }
    }

    /**
     * Returns true if an elevation line should be drawn between the starting
     * hex and the hex in the direction specified. Results should be transitive,
     * that is, if a line is drawn in one direction, it should be drawn in the
     * opposite direction as well.
     */
    private final boolean drawElevationLine(Coords src, int direction) {
        final IHex srcHex = game.getBoard().getHex(src);
        final IHex destHex = game.getBoard().getHexInDir(src, direction);
        return (destHex != null) && (srcHex.floor() != destHex.floor());
    }

    /**
     * Returns the absolute position of the upper-left hand corner of the hex
     * graphic
     */
    private Point getHexLocation(int x, int y) {
        return new Point(x * (int) (HEX_WC * scale), y * (int) (HEX_H * scale)
                + ((x & 1) == 1 ? (int) (HEX_H / 2 * scale) : 0));
    }

    Point getHexLocation(Coords c) {
        return getHexLocation(c.x, c.y);
    }

    /**
     * Returns the absolute position of the centre of the hex graphic
     */
    private Point getCentreHexLocation(int x, int y) {
        Point p = getHexLocation(x, y);
        p.x += (HEX_W / 2 * scale);
        p.y += (HEX_H / 2 * scale);
        return p;
    }

    private Point getCentreHexLocation(Coords c) {
        return getCentreHexLocation(c.x, c.y);
    }

    /**
     * Returns the coords at the specified point
     */
    Coords getCoordsAt(Point p) {
        final int x = (p.x + scroll.x - offset.x) / (int) (HEX_WC * scale);
        final int y = ((p.y + scroll.y - offset.y) - ((x & 1) == 1 ? (int) (HEX_H / 2 * scale)
                : 0))
                / (int) (HEX_H * scale);
        return new Coords(x, y);
    }

    /**
     * Shows the tooltip thinger
     */
    private void showTooltip() {
        try {
            final Point tipLoc = new Point(getLocationOnScreen());
            // retrieve tip text
            String[] tipText = getTipText(mousePos);
            if (tipText == null) {
                return;
            }

            // update tip text
            tipWindow.removeAll();
            tipWindow.add(new TooltipCanvas(tipText));
            tipWindow.pack();

            tipLoc.translate(mousePos.x, mousePos.y + 20);

            // adjust horizontal location for the tipWindow if it goes off the
            // frame
            if (getLocationOnScreen().x + this.getSize().width < tipLoc.x
                    + tipWindow.getSize().width + 10) {
                if (this.getSize().width > tipWindow.getSize().width) {
                    // bound it by the right edge of the frame
                    tipLoc.x -= tipLoc.x + tipWindow.getSize().width + 10
                    - this.getSize().width - this.getLocation().x;
                } else {
                    // too big to fit, left justify to the frame (roughly).
                    // how do I extract the first term of HEX_SIZE to use
                    // here?--LDE
                    tipLoc.x = getLocationOnScreen().x + hex_size.width;
                }
            }

            // set tip location
            tipWindow.setLocation(tipLoc);

            tipWindow.setVisible(true);
        } catch (Exception e) {
            e.printStackTrace();
            tipWindow = new Window(null);
        }
    }

    /**
     * The text to be displayed when the mouse is at a certain point
     */
    private String[] getTipText(Point point) {

        int stringsSize = 0;
        IHex mhex = null;

        // first, we have to determine how much text we are going to have
        // are we on a hex?
        final Coords mcoords = getCoordsAt(point);
        if (GUIPreferences.getInstance().getShowMapHexPopup()
                && game.getBoard().contains(mcoords)) {
            mhex = game.getBoard().getHex(mcoords);
            stringsSize += 1;
        }

        // check if it's on any entities
        for (EntitySprite eSprite : entitySprites) {
            if (eSprite.isInside(point)) {
                stringsSize += 3;
            }
        }

        // check if it's on any attacks
        for (AttackSprite aSprite : attackSprites) {
            if (aSprite.isInside(point)) {
                stringsSize += 1 + aSprite.weaponDescs.size();
            }
        }

        // If the hex contains a building, make more space.
        // Also if it contains other displayable terrain.
        if (mhex != null) {
            stringsSize += mhex.displayableTerrainsPresent();
            if (mhex.containsTerrain(Terrains.BUILDING)) {
                stringsSize++;
            }
            if (mhex.containsTerrain(Terrains.FUEL_TANK)) {
                stringsSize++;
            }
            if (mhex.containsTerrain(Terrains.BRIDGE)) {
                stringsSize++;
            }
        }

        stringsSize += game.getNbrMinefields(mcoords);

        // Artillery
        final ArrayList<ArtilleryAttackAction> artilleryAttacks = getArtilleryAttacksAtLocation(mcoords);
        stringsSize += artilleryAttacks.size();

        // Artillery fire adjustment
        final Mounted curWeapon = getSelectedArtilleryWeapon();
        if (curWeapon != null) {
            stringsSize++;
        }

        /*
         * Eventaul replacemtn for the artilery popup.
         *
        final Collection<SpecialHexDisplay> specials = game.getBoard().getSpecialHexDisplay(mcoords);
        if(specials != null)
                stringsSize += specials.size();

        // if the size is zip, you must a'quit
        if (stringsSize == 0) {
            return null;
        }*/

        // now we can allocate an array of strings
        String[] strings = new String[stringsSize];
        int stringsIndex = 0;

        // are we on a hex?
        if (mhex != null) {
            strings[stringsIndex] = Messages.getString("BoardView1.Hex") + mcoords.getBoardNum() //$NON-NLS-1$
            + Messages.getString("BoardView1.level") + mhex.getElevation(); //$NON-NLS-1$
            stringsIndex += 1;

            //cycle through the terrains and report types found
            //this will skip buildings and other constructed units
            for(int i=0;i < Terrains.SIZE; i++) {
                if(mhex.containsTerrain(i)) {
                    int tf = mhex.getTerrain(i).getTerrainFactor();
                    int ttl = mhex.getTerrain(i).getLevel();
                    String name = Terrains.getDisplayName(i, ttl);
                    if(tf > 0) {
                        name = name + " (" + tf + ")";
                    }
                    if(null != name) {
                        strings[stringsIndex] = name;
                        stringsIndex += 1;
                    }
                }
            }

            // Do we have a building?
            if (mhex.containsTerrain(Terrains.FUEL_TANK)) {
                // Get the building.
                Building bldg = game.getBoard().getBuildingAt(mcoords);
                StringBuffer buf = new StringBuffer(Messages
                        .getString("BoardView1.Height")); //$NON-NLS-1$
                // Each hex of a building has its own elevation.
                buf.append(mhex.terrainLevel(Terrains.FUEL_TANK_ELEV));
                buf.append(" "); //$NON-NLS-1$
                buf.append(bldg.toString());
                buf.append(Messages.getString("BoardView1.CF")); //$NON-NLS-1$
                buf.append(bldg.getCurrentCF(mcoords));
                strings[stringsIndex] = buf.toString();
                stringsIndex += 1;
            }
            if (mhex.containsTerrain(Terrains.BUILDING)) {
                // Get the building.
                Building bldg = game.getBoard().getBuildingAt(mcoords);
                StringBuffer buf = new StringBuffer(Messages
                        .getString("BoardView1.Height")); //$NON-NLS-1$
                // Each hex of a building has its own elevation.
                buf.append(mhex.terrainLevel(Terrains.BLDG_ELEV));
                buf.append(" "); //$NON-NLS-1$
                buf.append(bldg.toString());
                buf.append(Messages.getString("BoardView1.CF")); //$NON-NLS-1$
                buf.append(bldg.getCurrentCF(mcoords));
                strings[stringsIndex] = buf.toString();
                stringsIndex += 1;
            }

            // Do we have a bridge?
            if (mhex.containsTerrain(Terrains.BRIDGE)) {
                // Get the building.
                Building bldg = game.getBoard().getBuildingAt(mcoords);
                StringBuffer buf = new StringBuffer(Messages
                        .getString("BoardView1.Height")); //$NON-NLS-1$
                // Each hex of a building has its own elevation.
                buf.append(mhex.terrainLevel(Terrains.BRIDGE_ELEV));
                buf.append(" "); //$NON-NLS-1$
                buf.append(bldg.toString());
                buf.append(Messages.getString("BoardView1.CF")); //$NON-NLS-1$
                buf.append(bldg.getCurrentCF(mcoords));
                strings[stringsIndex] = buf.toString();
                stringsIndex += 1;
            }

            if (game.containsMinefield(mcoords)) {
                Vector<Minefield> minefields = game.getMinefields(mcoords);
                for (int i = 0; i < minefields.size(); i++) {
                    Minefield mf = minefields.elementAt(i);
                    String owner = " (" + game.getPlayer(mf.getPlayerId()).getName() + ")"; //$NON-NLS-1$ //$NON-NLS-2$

                    switch (mf.getType()) {
                    case (Minefield.TYPE_CONVENTIONAL):
                        strings[stringsIndex] = mf.getName()
                        + Messages
                        .getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + " " + owner; //$NON-NLS-1$ //$NON-NLS-2$
                    break;
                    case (Minefield.TYPE_COMMAND_DETONATED):
                        strings[stringsIndex] = mf.getName()
                        + Messages
                        .getString("BoardView1.minefield") + "(" + mf.getDensity() + ")"+ " " + owner; //$NON-NLS-1$ //$NON-NLS-2$
                    break;
                    case (Minefield.TYPE_VIBRABOMB):
                        if (mf.getPlayerId() == localPlayer.getId()) {
                            strings[stringsIndex] = mf.getName()
                            + Messages
                            .getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + "(" + mf.getSetting() + ") " + owner; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                        } else {
                            strings[stringsIndex] = mf.getName()
                            + Messages
                            .getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + " " + owner; //$NON-NLS-1$ //$NON-NLS-2$
                        }
                    break;
                    case (Minefield.TYPE_ACTIVE):
                        strings[stringsIndex] = mf.getName()
                        + Messages
                        .getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + owner; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    break;
                    case (Minefield.TYPE_INFERNO):
                        strings[stringsIndex] = mf.getName()
                        + Messages
                        .getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + owner; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    break;
                    }
                    stringsIndex++;
                }
            }
        }
        // check if it's on any entities
        for (EntitySprite eSprite : entitySprites) {
            if (eSprite.isInside(point)) {
                final String[] entityStrings = eSprite.getTooltip();
                System.arraycopy(entityStrings, 0, strings, stringsIndex,
                        entityStrings.length);
                stringsIndex += entityStrings.length;
            }
        }

        // check if it's on any attacks
        for (AttackSprite aSprite : attackSprites) {
            if (aSprite.isInside(point)) {
                final String[] attackStrings = aSprite.getTooltip();
                System.arraycopy(attackStrings, 0, strings, stringsIndex,
                        attackStrings.length);
                stringsIndex += 1 + aSprite.weaponDescs.size();
            }
        }

        // check artillery attacks
        for (ArtilleryAttackAction aaa : artilleryAttacks) {
            final Entity ae = game.getEntity(aaa.getEntityId());
            String s = null;
            if (ae != null) {
                if (aaa.getWeaponId() > -1) {
                    Mounted weap = ae.getEquipment(aaa.getWeaponId());
                    s = weap.getName();
                    if (aaa.getAmmoId() > -1) {
                        Mounted ammo = ae.getEquipment(aaa.getAmmoId());
                        s += "(" + ammo.getName() + ")";
                    }
                }
            }
            if (s == null) {
                s = Messages.getString("BoardView1.Artillery");
            }
            strings[stringsIndex++] = Messages.getString(
                    "BoardView1.ArtilleryAttack", new Object[] { s,
                            new Integer(aaa.turnsTilHit),
                            aaa.toHit(game).getValueAsString() });
        }

        // check artillery fire adjustment
        if ((curWeapon != null) && (selectedEntity != null)) {
            // process targetted hexes
            int amod = 0;
            // Check the predesignated hexes
            if (selectedEntity.getOwner().getArtyAutoHitHexes().contains(
                    mcoords)) {
                amod = TargetRoll.AUTOMATIC_SUCCESS;
            } else {
                amod = selectedEntity.aTracker.getModifier(curWeapon, mcoords);
            }

            if (amod == TargetRoll.AUTOMATIC_SUCCESS) {
                strings[stringsIndex++] = Messages
                .getString("BoardView1.ArtilleryAutohit");
            } else {
                strings[stringsIndex++] = Messages.getString(
                        "BoardView1.ArtilleryAdjustment",
                        new Object[] { new Integer(amod) });
            }
        }
        return strings;
    }

    /**
     * Hides the tooltip thinger
     */
    public void hideTooltip() {
        tipWindow.setVisible(false);
    }

    /**
     * Returns true if the tooltip is showing
     */
    private boolean isTipShowing() {
        return tipWindow.isShowing();
    }

    /**
     * Checks if the mouse has been idling for a while and if so, shows the
     * tooltip window
     */
    void checkTooltip() {
        if (isTipShowing()) {
            if (!isTipPossible) {
                hideTooltip();
            }
        } else if (isTipPossible
                && (System.currentTimeMillis() - lastIdle > GUIPreferences
                .getInstance().getTooltipDelay())) {
            showTooltip();
        }
    }

    public void redrawMovingEntity(Entity entity, Coords position, int facing) {
        Integer entityId = new Integer(entity.getId());
        EntitySprite sprite = entitySpriteIds.get(entityId);
        ArrayList<EntitySprite> newSprites;
        HashMap<Integer, EntitySprite> newSpriteIds;

        if (sprite != null) {
            newSprites = new ArrayList<EntitySprite>(entitySprites);
            newSpriteIds = new HashMap<Integer, EntitySprite>(entitySpriteIds);

            newSprites.remove(sprite);

            entitySprites = newSprites;
            entitySpriteIds = newSpriteIds;
        }

        MovingEntitySprite mSprite = movingEntitySpriteIds.get(entityId);
        ArrayList<MovingEntitySprite> newMovingSprites = new ArrayList<MovingEntitySprite>(
                movingEntitySprites);
        HashMap<Integer, MovingEntitySprite> newMovingSpriteIds = new HashMap<Integer, MovingEntitySprite>(
                movingEntitySpriteIds);

        if (mSprite != null) {
            newMovingSprites.remove(mSprite);
        }

        if (entity.getPosition() != null) {
            mSprite = new MovingEntitySprite(entity, position, facing);
            newMovingSprites.add(mSprite);
            newMovingSpriteIds.put(entityId, mSprite);
        }

        movingEntitySprites = newMovingSprites;
        movingEntitySpriteIds = newMovingSpriteIds;
    }

    public boolean isMovingUnits() {
        return movingUnits.size() > 0;
    }

    /**
     * Clears the sprite for an entity and prepares it to be re-drawn. Replaces
     * the old sprite with the new! Try to prevent annoying
     * ConcurrentModificationExceptions
     */
    public synchronized void redrawEntity(Entity entity) {
        Integer entityId = new Integer(entity.getId());
        EntitySprite sprite = entitySpriteIds.get(entityId);
        ArrayList<EntitySprite> newSprites = new ArrayList<EntitySprite>(
                entitySprites);
        HashMap<Integer, EntitySprite> newSpriteIds = new HashMap<Integer, EntitySprite>(
                entitySpriteIds);

        if (sprite != null) {
            newSprites.remove(sprite);
        }
        Coords position = entity.getPosition();
        if (position != null) {
            sprite = new EntitySprite(entity);
            newSprites.add(sprite);
            newSpriteIds.put(entityId, sprite);
        }

        entitySprites = newSprites;
        entitySpriteIds = newSpriteIds;
        for (Iterator<C3Sprite> i = C3Sprites.iterator(); i.hasNext();) {
            final C3Sprite c3sprite = i.next();
            if ((c3sprite.entityId == entity.getId())
                    || (c3sprite.masterId == entity.getId())) {
                i.remove();
            }
        }

        if (entity.hasC3() || entity.hasC3i()) {
            addC3Link(entity);
        }

        scheduleRedraw();
    }

    /**
     * Clears all old entity sprites out of memory and sets up new ones.
     */
    void redrawAllEntities() {
        ArrayList<EntitySprite> newSprites = new ArrayList<EntitySprite>(game
                .getNoOfEntities());
        HashMap<Integer, EntitySprite> newSpriteIds = new HashMap<Integer, EntitySprite>(
                game.getNoOfEntities());
        ArrayList<WreckSprite> newWrecks = new ArrayList<WreckSprite>();

        Enumeration<Entity> e = game.getWreckedEntities();
        while (e.hasMoreElements()) {
            Entity entity = e.nextElement();
            if (!(entity instanceof Infantry) && (entity.getPosition() != null)) {
                WreckSprite ws = new WreckSprite(entity);
                newWrecks.add(ws);
            }
        }

        clearC3Networks();
        for (Enumeration<Entity> i = game.getEntities(); i.hasMoreElements();) {
            final Entity entity = i.nextElement();
            if (entity.getPosition() == null) {
                continue;
            }

            EntitySprite sprite = new EntitySprite(entity);
            newSprites.add(sprite);
            newSpriteIds.put(new Integer(entity.getId()), sprite);

            if (entity.hasC3() || entity.hasC3i()) {
                addC3Link(entity);
            }
        }

        entitySprites = newSprites;
        entitySpriteIds = newSpriteIds;
        wreckSprites = newWrecks;

        scheduleRedraw();
    }

    /**
     * Moves the cursor to the new position, or hides it, if newPos is null
     */
    private void moveCursor(CursorSprite cursor, Coords newPos) {
        final Rectangle oldBounds = new Rectangle(cursor.getBounds());
        if (newPos != null) {
            // cursor.setLocation(getHexLocation(newPos));
            cursor.setHexLocation(newPos);
        } else {
            cursor.setOffScreen();
        }
        // repaint affected area
        repaintBounds(oldBounds);
        repaintBounds(cursor.getBounds());
    }

    public void centerOnHex(Coords c) {
        if (null == c) {
            return;
        }
        scroll.setLocation(getHexLocation(c));
        scroll.translate((int) (42 * scale) - (view.width / 2),
                (int) (36 * scale) - (view.height / 2));

        isScrolling = false;
        checkScrollBounds();
        repaint();
    }

    /**
     * Clears the old movement data and draws the new. Since it's less expensive
     * to check for and reuse old step sprites than to make a whole new one, we
     * do that.
     */
    public void drawMovementData(Entity entity, MovePath md) {
        ArrayList<StepSprite> temp = pathSprites;
        MoveStep previousStep = null;

        clearMovementData();

        //need to update the movement sprites based on the move path for this entity
        //only way to do this is to clear and refresh (seems wasteful)

        //first get the color for the vector
        Color col = Color.blue;
        if(md.getLastStep() != null) {
            switch (md.getLastStep().getMovementType()) {
            case IEntityMovementType.MOVE_RUN:
            case IEntityMovementType.MOVE_VTOL_RUN:
            case IEntityMovementType.MOVE_OVER_THRUST:
                col = GUIPreferences.getInstance().getColor("AdvancedMoveRunColor");
                break;
            case IEntityMovementType.MOVE_JUMP :
                col = GUIPreferences.getInstance().getColor("AdvancedMoveJumpColor");
                break;
            case IEntityMovementType.MOVE_ILLEGAL :
                col = GUIPreferences.getInstance().getColor("AdvancedMoveIllegalColor");
                break;
            default :
                col = GUIPreferences.getInstance().getColor("AdvancedMoveDefaultColor");
            break;
            }
        }

        refreshMoveVectors(entity, md, col);

        for (Enumeration<MoveStep> i = md.getSteps(); i.hasMoreElements();) {
            final MoveStep step = i.nextElement();
            // check old movement path for reusable step sprites
            boolean found = false;
            for (StepSprite sprite : temp) {
                if (sprite.getStep().canReuseSprite(step) && !(entity instanceof Aero)) {
                    pathSprites.add(sprite);
                    found = true;
                }
            }
            if (!found) {
                if ((null != previousStep) &&
                        ((step.getType() == MovePath.STEP_UP) ||
                                (step.getType() == MovePath.STEP_DOWN) ||
                                (step.getType() == MovePath.STEP_ACC) ||
                                (step.getType() == MovePath.STEP_DEC) ||
                                (step.getType() == MovePath.STEP_ACCN) ||
                                (step.getType() == MovePath.STEP_DECN))) {
                    //Mark the previous elevation change sprite hidden
                    // so that we can draw a new one in it's place without
                    // having overlap.
                    pathSprites.get(pathSprites.size() -1 ).hidden = true;
                }

                //for advanced movement, we always need to hide prior
                //because costs will overlap and we only want the current facing
                if((previousStep != null) && game.useVectorMove()) {
                    pathSprites.get(pathSprites.size() -1 ).hidden = true;
                }

                pathSprites.add(new StepSprite(step));
            }
            previousStep = step;
        }
        repaint(100);
    }

    /**
     * Clears current movement data from the screen
     */
    public void clearMovementData() {
        ArrayList<StepSprite> temp = pathSprites;
        pathSprites = new ArrayList<StepSprite>();
        for (Sprite sprite : temp) {
            repaintBounds(sprite.getBounds());
        }
        refreshMoveVectors();
    }

    public void setLocalPlayer(Player p) {
        localPlayer = p;
    }

    public Player getLocalPlayer() {
        return localPlayer;
    }

    /**
     * Specifies that this should mark the deployment hexes for a player. If the
     * player is set to null, no hexes will be marked.
     */
    public void markDeploymentHexesFor(Player p) {
        m_plDeployer = p;
    }

    /**
     * Adds a c3 line to the sprite list.
     */
    public void addC3Link(Entity e) {
        if (e.getPosition() == null) {
            return;
        }

        if (e.hasC3i()) {
            for (java.util.Enumeration<Entity> i = game.getEntities(); i
            .hasMoreElements();) {
                final Entity fe = i.nextElement();
                if (fe.getPosition() == null) {
                    return;
                }
                if (e.onSameC3NetworkAs(fe) && !fe.equals(e) &&
                        !Compute.isAffectedByECM(e, e.getPosition(), fe.getPosition())) {
                    C3Sprites.add(new C3Sprite(e, fe));
                }
            }
        } else if (e.getC3Master() != null) {
            Entity eMaster = e.getC3Master();
            if (eMaster.getPosition() == null) {
                return;
            }

            // ECM cuts off the network
            if (!Compute.isAffectedByECM(e, e.getPosition(), eMaster
                    .getPosition())
                    && !Compute.isAffectedByECM(eMaster, eMaster.getPosition(),
                            eMaster.getPosition())) {
                C3Sprites.add(new C3Sprite(e, e.getC3Master()));
            }
        }
    }

    /**
     * Adds an attack to the sprite list.
     */
    public synchronized void addAttack(AttackAction aa) {
        // do not make a sprite unless we're aware of both entities
        // this is not a great solution but better than a crash
        Entity ae = game.getEntity(aa.getEntityId());
        Targetable t = game.getTarget(aa.getTargetType(), aa.getTargetId());
        if ((ae == null) || (t == null)
                || (t.getTargetType() == Targetable.TYPE_INARC_POD)
                || (t.getPosition() == null) || (ae.getPosition() == null)) {
            return;
        }

        for (AttackSprite sprite : attackSprites) {
            // can we just add this attack to an existing one?
            if ((sprite.getEntityId() == aa.getEntityId())
                    && (sprite.getTargetId() == aa.getTargetId())) {
                // use existing attack, but add this weapon
                if (aa instanceof WeaponAttackAction) {
                    WeaponAttackAction waa = (WeaponAttackAction) aa;
                    if (aa.getTargetType() != Targetable.TYPE_HEX_ARTILLERY) {
                        sprite.addWeapon(waa);
                    } else if (waa.getEntity(game).getOwner().getId() == localPlayer
                            .getId()) {
                        sprite.addWeapon(waa);
                    }
                }
                if (aa instanceof KickAttackAction) {
                    sprite.addWeapon((KickAttackAction) aa);
                }
                if (aa instanceof PunchAttackAction) {
                    sprite.addWeapon((PunchAttackAction) aa);
                }
                if (aa instanceof PushAttackAction) {
                    sprite.addWeapon((PushAttackAction) aa);
                }
                if (aa instanceof ClubAttackAction) {
                    sprite.addWeapon((ClubAttackAction) aa);
                }
                if (aa instanceof ChargeAttackAction) {
                    sprite.addWeapon((ChargeAttackAction) aa);
                }
                if (aa instanceof DfaAttackAction) {
                    sprite.addWeapon((DfaAttackAction) aa);
                }
                if (aa instanceof ProtomechPhysicalAttackAction) {
                    sprite.addWeapon((ProtomechPhysicalAttackAction) aa);
                }
                if (aa instanceof SearchlightAttackAction) {
                    sprite.addWeapon((SearchlightAttackAction) aa);
                }
                return;
            }
        }
        // no re-use possible, add a new one
        // don't add a sprite for an artillery attack made by the other player
        if (aa instanceof WeaponAttackAction) {
            WeaponAttackAction waa = (WeaponAttackAction) aa;
            if (aa.getTargetType() != Targetable.TYPE_HEX_ARTILLERY) {
                attackSprites.add(new AttackSprite(aa));
            } else if (waa.getEntity(game).getOwner().getId() == localPlayer
                    .getId()) {
                attackSprites.add(new AttackSprite(aa));
            }
        } else {
            attackSprites.add(new AttackSprite(aa));
        }
    }

    /** Removes all attack sprites from a certain entity */
    public synchronized void removeAttacksFor(Entity e) {
        if (e == null) {
            return;
        }
        int entityId = e.getId();
        for (Iterator<AttackSprite> i = attackSprites.iterator(); i.hasNext();) {
            AttackSprite sprite = i.next();
            if (sprite.getEntityId() == entityId) {
                i.remove();
            }
        }
        repaint(100);
    }

    /**
     * Clears out all attacks and re-adds the ones in the current game.
     */
    public void refreshAttacks() {
        clearAllAttacks();
        for (Enumeration<EntityAction> i = game.getActions(); i
        .hasMoreElements();) {
            EntityAction ea = i.nextElement();
            if (ea instanceof AttackAction) {
                addAttack((AttackAction) ea);
            }
        }
        for (Enumeration<AttackAction> i = game.getCharges(); i
        .hasMoreElements();) {
            AttackAction ea = i.nextElement();
            if (ea instanceof PhysicalAttackAction) {
                addAttack(ea);
            }
        }
    }

    public void refreshMoveVectors() {
        clearAllMoveVectors();
        for(Enumeration<Entity> i = game.getEntities(); i.hasMoreElements();) {
            Entity e = i.nextElement();
            if(e.getPosition() != null) {
                movementSprites.add(new MovementSprite(e, e.getVectors(), Color.gray, false));
            }
        }
    }

    public void refreshMoveVectors(Entity en, MovePath md, Color col) {
        clearAllMoveVectors();
        //same as normal but when I find the active entity I used the MovePath
        //to get vector
        for(Enumeration<Entity> i = game.getEntities(); i.hasMoreElements();) {
            Entity e = i.nextElement();
            if(e.getPosition() != null) {
                if(e.getId() == en.getId()) {
                    movementSprites.add(new MovementSprite(e, md.getFinalVectors(), col, true));
                } else {
                    movementSprites.add(new MovementSprite(e, e.getVectors(), col, false));
                }
            }
        }
    }

    public void clearC3Networks() {
        C3Sprites.clear();
    }

    /**
     * Clears out all attacks that were being drawn
     */
    public void clearAllAttacks() {
        attackSprites.clear();
    }

    /**
     * Clears out all movement vectors that were being drawn
     */
    public void clearAllMoveVectors() {
        movementSprites.clear();
    }

    protected void firstLOSHex(Coords c) {
        if (useLOSTool) {
            moveCursor(secondLOSSprite, null);
            moveCursor(firstLOSSprite, c);
        }
    }

    protected void secondLOSHex(Coords c2, Coords c1) {
        if (useLOSTool) {
            moveCursor(firstLOSSprite, c1);
            moveCursor(secondLOSSprite, c2);
            Entity ae = chooseEntity(c1);
            Entity te = chooseEntity(c2);

            StringBuffer message = new StringBuffer();
            LosEffects le;
            if ((ae == null) || (te == null)) {
                boolean mechInFirst = GUIPreferences.getInstance()
                .getMechInFirst();
                boolean mechInSecond = GUIPreferences.getInstance()
                .getMechInSecond();
                LosEffects.AttackInfo ai = new LosEffects.AttackInfo();
                ai.attackPos = c1;
                ai.targetPos = c2;
                ai.attackHeight = mechInFirst ? 1 : 0;
                ai.targetHeight = mechInSecond ? 1 : 0;
                ai.attackAbsHeight = game.getBoard().getHex(c1).floor()
                + ai.attackHeight;
                ai.targetAbsHeight = game.getBoard().getHex(c2).floor()
                + ai.targetHeight;
                le = LosEffects.calculateLos(game, ai);
                message
                .append(Messages
                        .getString(
                                "BoardView1.Attacker", new Object[] { //$NON-NLS-1$
                                        mechInFirst ? Messages
                                                .getString("BoardView1.Mech") : Messages.getString("BoardView1.NonMech"), //$NON-NLS-1$ //$NON-NLS-2$
                                                c1.getBoardNum() }));
                message
                .append(Messages
                        .getString(
                                "BoardView1.Target", new Object[] { //$NON-NLS-1$
                                        mechInSecond ? Messages
                                                .getString("BoardView1.Mech") : Messages.getString("BoardView1.NonMech"), //$NON-NLS-1$ //$NON-NLS-2$
                                                c2.getBoardNum() }));
            } else {
                le = LosEffects.calculateLos(game, ae.getId(), te);
                message.append(Messages.getString(
                        "BoardView1.Attacker", new Object[] { //$NON-NLS-1$
                                ae.getDisplayName(), c1.getBoardNum() }));
                message.append(Messages.getString(
                        "BoardView1.Target", new Object[] { //$NON-NLS-1$
                                te.getDisplayName(), c2.getBoardNum() }));
            }
            if (le.isBlocked()) {
                message.append(Messages.getString(
                        "BoardView1.LOSBlocked", new Object[] { //$NON-NLS-1$
                                new Integer(c1.distance(c2)) }));
            } else {
                message.append(Messages.getString(
                        "BoardView1.LOSNotBlocked", new Object[] { //$NON-NLS-1$
                                new Integer(c1.distance(c2)) }));
                if (le.getHeavyWoods() > 0) {
                    message.append(Messages.getString(
                            "BoardView1.HeavyWoods", new Object[] { //$NON-NLS-1$
                                    new Integer(le.getHeavyWoods()) }));
                }
                if (le.getLightWoods() > 0) {
                    message.append(Messages.getString(
                            "BoardView1.LightWoods", new Object[] { //$NON-NLS-1$
                                    new Integer(le.getLightWoods()) }));
                }
                if (le.getLightSmoke() > 0) {
                    message.append(Messages.getString(
                            "BoardView1.LightSmoke", new Object[] { //$NON-NLS-1$
                                    new Integer(le.getLightSmoke()) }));
                }
                if (le.getHeavySmoke() > 0) {
                    message.append(Messages.getString(
                            "BoardView1.HeavySmoke", new Object[] { //$NON-NLS-1$
                                    new Integer(le.getHeavySmoke()) }));
                }
                if (le.isTargetCover()) {
                    message.append(Messages
                            .getString("BoardView1.TargetPartialCover")); //$NON-NLS-1$
                }
                if (le.isAttackerCover()) {
                    message.append(Messages
                            .getString("BoardView1.AttackerPartialCover")); //$NON-NLS-1$
                }
            }
            AlertDialog alert = new AlertDialog((Frame)SwingUtilities.getAncestorOfClass(Frame.class, this), Messages
                    .getString("BoardView1.LOSTitle"), //$NON-NLS-1$
                    message.toString(), false);
            alert.setVisible(true);
        }
    }

    /**
     * If the mouse is at the edges of the screen, this scrolls the board image
     * on the canvas. NOTE: CTL scroll is handled in mouseMoved()
     */
    public boolean doScroll() {
        final Point oldScroll = new Point(scroll);
        boolean s = false;

        if (isScrolling && GUIPreferences.getInstance().getRightDragScroll()) {
            if (!((oldMousePosition == null) || mousePos.equals(oldMousePosition))) {
                scroll.x -= GUIPreferences.getInstance().getScrollSensitivity()
                * (mousePos.x - oldMousePosition.x);
                scroll.y -= GUIPreferences.getInstance().getScrollSensitivity()
                * (mousePos.y - oldMousePosition.y);
                checkScrollBounds();
                oldMousePosition.setLocation(mousePos);
                s = !oldScroll.equals(scroll);
                scrolled = scrolled || s;
            }
        }

        if (isScrolling
                && (GUIPreferences.getInstance().getClickEdgeScroll() || GUIPreferences
                        .getInstance().getAutoEdgeScroll())) {
            final int sf = GUIPreferences.getInstance().getScrollSensitivity(); // scroll
            // factor
            // adjust x scroll
            // scroll when the mouse is at the edges
            if (mousePos.x < 100) {
                scroll.x -= (100 - mousePos.x) / sf;
            } else if (mousePos.x > (backSize.width - 100)) {
                scroll.x -= ((backSize.width - 100) - mousePos.x) / sf;
            }
            // scroll when the mouse is at the edges
            if (mousePos.y < 100) {
                scroll.y -= (100 - mousePos.y) / sf;
            } else if (mousePos.y > (backSize.height - 100)) {
                scroll.y -= ((backSize.height - 100) - mousePos.y) / sf;
            }
            checkScrollBounds();
            if (!oldScroll.equals(scroll)) {
                // repaint();
                s = true;
                scrolled = s;
            }
        }

        return s;
    }

    /**
     * Makes sure that the scroll dimensions stay in bounds
     */
    public void checkScrollBounds() {
        if (scroll.x < 0) {
            scroll.x = 0;
        } else if (scroll.x > (boardSize.width - view.width)) {
            scroll.x = (boardSize.width - view.width);
        }

        if (scroll.y < 0) {
            scroll.y = 0;
        } else if (scroll.y > (boardSize.height - view.height)) {
            scroll.y = (boardSize.height - view.height);
        }

        // Update our scroll bars.
        if (null != vScrollbar) {
            vScrollbar.setValue(scroll.y);
        }
        if (null != hScrollbar) {
            hScrollbar.setValue(scroll.x);
        }
    }

    protected void stopScrolling() {
        isScrolling = false;
    }

    /**
     * Initializes the various overlay polygons with their vertices.
     */
    public void initPolys() {
        // hex polygon
        hexPoly = new Polygon();
        hexPoly.addPoint(21, 0);
        hexPoly.addPoint(62, 0);
        hexPoly.addPoint(83, 35);
        hexPoly.addPoint(83, 36);
        hexPoly.addPoint(62, 71);
        hexPoly.addPoint(21, 71);
        hexPoly.addPoint(0, 36);
        hexPoly.addPoint(0, 35);

        // facing polygons
        facingPolys = new Polygon[6];
        facingPolys[0] = new Polygon();
        facingPolys[0].addPoint(41, 3);
        facingPolys[0].addPoint(38, 6);
        facingPolys[0].addPoint(45, 6);
        facingPolys[0].addPoint(42, 3);
        facingPolys[1] = new Polygon();
        facingPolys[1].addPoint(69, 17);
        facingPolys[1].addPoint(64, 17);
        facingPolys[1].addPoint(68, 23);
        facingPolys[1].addPoint(70, 19);
        facingPolys[2] = new Polygon();
        facingPolys[2].addPoint(69, 53);
        facingPolys[2].addPoint(68, 49);
        facingPolys[2].addPoint(64, 55);
        facingPolys[2].addPoint(68, 54);
        facingPolys[3] = new Polygon();
        facingPolys[3].addPoint(41, 68);
        facingPolys[3].addPoint(38, 65);
        facingPolys[3].addPoint(45, 65);
        facingPolys[3].addPoint(42, 68);
        facingPolys[4] = new Polygon();
        facingPolys[4].addPoint(15, 53);
        facingPolys[4].addPoint(18, 54);
        facingPolys[4].addPoint(15, 48);
        facingPolys[4].addPoint(14, 52);
        facingPolys[5] = new Polygon();
        facingPolys[5].addPoint(13, 19);
        facingPolys[5].addPoint(15, 23);
        facingPolys[5].addPoint(19, 17);
        facingPolys[5].addPoint(17, 17);

        // movement polygons
        movementPolys = new Polygon[8];
        movementPolys[0] = new Polygon();
        movementPolys[0].addPoint(41, 65);
        movementPolys[0].addPoint(38, 68);
        movementPolys[0].addPoint(45, 68);
        movementPolys[0].addPoint(42, 65);
        movementPolys[1] = new Polygon();
        movementPolys[1].addPoint(17, 48);
        movementPolys[1].addPoint(12, 48);
        movementPolys[1].addPoint(16, 54);
        movementPolys[1].addPoint(17, 49);
        movementPolys[2] = new Polygon();
        movementPolys[2].addPoint(18, 19);
        movementPolys[2].addPoint(17, 15);
        movementPolys[2].addPoint(13, 21);
        movementPolys[2].addPoint(17, 20);
        movementPolys[3] = new Polygon();
        movementPolys[3].addPoint(41, 6);
        movementPolys[3].addPoint(38, 3);
        movementPolys[3].addPoint(45, 3);
        movementPolys[3].addPoint(42, 6);
        movementPolys[4] = new Polygon();
        movementPolys[4].addPoint(67, 15);
        movementPolys[4].addPoint(66, 19);
        movementPolys[4].addPoint(67, 20);
        movementPolys[4].addPoint(71, 20);
        movementPolys[5] = new Polygon();
        movementPolys[5].addPoint(69, 55);
        movementPolys[5].addPoint(66, 50);
        movementPolys[5].addPoint(67, 49);
        movementPolys[5].addPoint(72, 48);

        movementPolys[6] = new Polygon(); // up arrow with tail
        movementPolys[6].addPoint(35, 44);
        movementPolys[6].addPoint(30, 49);
        movementPolys[6].addPoint(33, 49);
        movementPolys[6].addPoint(33, 53);
        movementPolys[6].addPoint(38, 53);
        movementPolys[6].addPoint(38, 49);
        movementPolys[6].addPoint(41, 49);
        movementPolys[6].addPoint(36, 44);
        movementPolys[7] = new Polygon(); // down arrow with tail
        movementPolys[7].addPoint(34, 53);
        movementPolys[7].addPoint(29, 48);
        movementPolys[7].addPoint(32, 48);
        movementPolys[7].addPoint(32, 44);
        movementPolys[7].addPoint(37, 44);
        movementPolys[7].addPoint(37, 48);
        movementPolys[7].addPoint(40, 48);
        movementPolys[7].addPoint(35, 53);
    }

    synchronized boolean doMoveUnits(long idleTime) {
        boolean movingSomething = false;

        if (movingUnits.size() > 0) {

            moveWait += idleTime;

            if (moveWait > GUIPreferences.getInstance().getInt(
            "AdvancedMoveStepDelay")) {

                ArrayList<MovingUnit> spent = new ArrayList<MovingUnit>();

                for (MovingUnit move : movingUnits) {
                    movingSomething = true;
                    Entity ge = game.getEntity(move.entity.getId());
                    if (move.path.size() > 0) {
                        UnitLocation loc = move.path.get(0);
                        if (ge != null) {
                            redrawMovingEntity(move.entity, loc.getCoords(),
                                    loc.getFacing());
                        }
                        move.path.remove(0);
                    } else {
                        if (ge != null) {
                            redrawEntity(ge);
                        }
                        spent.add(move);
                    }

                }

                for (MovingUnit move : spent) {
                    movingUnits.remove(move);
                }
                moveWait = 0;

                if (movingUnits.size() == 0) {
                    movingEntitySpriteIds.clear();
                    movingEntitySprites.clear();
                    ghostEntitySprites.clear();
                    processBoardViewEvent(new BoardViewEvent(this,
                            BoardViewEvent.FINISHED_MOVING_UNITS));
                }
            }
        }
        return movingSomething;
    }

    //
    // KeyListener
    //
    public void keyPressed(KeyEvent ke) {
        switch (ke.getKeyCode()) {
        case KeyEvent.VK_NUMPAD7:
            scroll.y -= HEX_H * scale;
            scroll.x -= HEX_W * scale;
            break;
        case KeyEvent.VK_NUMPAD8:
        case KeyEvent.VK_UP:
            scroll.y -= HEX_H * scale;
            break;
        case KeyEvent.VK_NUMPAD9:
            scroll.y -= HEX_H * scale;
            scroll.x += HEX_W * scale;
            break;
        case KeyEvent.VK_NUMPAD1:
            scroll.y += HEX_H * scale;
            scroll.x -= HEX_W * scale;
            break;
        case KeyEvent.VK_NUMPAD2:
        case KeyEvent.VK_DOWN:
            scroll.y += HEX_H * scale;
            break;
        case KeyEvent.VK_NUMPAD3:
            scroll.y += HEX_H * scale;
            scroll.x += HEX_W * scale;
            break;
        case KeyEvent.VK_NUMPAD4:
        case KeyEvent.VK_LEFT:
            scroll.x -= HEX_W * scale;
            break;
        case KeyEvent.VK_NUMPAD6:
        case KeyEvent.VK_RIGHT:
            scroll.x += HEX_W * scale;
            break;
        case KeyEvent.VK_NUMPAD5:
            // center on the selected entity
            if (selectedEntity != null) {
                centerOnHex(selectedEntity.getPosition());
            }
            break;
        case KeyEvent.VK_CONTROL:
            ctlKeyHeld = true;
            initCtlScroll = true;
            break;
        case KeyEvent.VK_PAGE_DOWN:
            zoomIn();
            break;
        case KeyEvent.VK_PAGE_UP:
            zoomOut();
            break;
        }

        if (isTipShowing()) {
            hideTooltip();
        }
        lastIdle = System.currentTimeMillis();
        checkScrollBounds();
        repaint();
    }

    public void keyReleased(KeyEvent ke) {
        if (ke.getKeyCode() == KeyEvent.VK_CONTROL) {
            ctlKeyHeld = false;
        }
    }

    public void keyTyped(KeyEvent ke) {
    }

    //
    // MouseListener
    //
    public void mousePressed(MouseEvent me) {
        scrolled = false; // not scrolled yet

        Point point = me.getPoint();
        if (null == point) {
            return;
        }
        oldMousePosition = point;

        isTipPossible = false;
        for (int i = 0; i < displayables.size(); i++) {
            IDisplayable disp = displayables.get(i);
            Point dispPoint = new Point();
            dispPoint.x = point.x - offset.x;
            dispPoint.y = point.y - offset.y;
            if ((backSize != null) && (disp.isHit(dispPoint, backSize))) {
                return;
            }
        }

        if (me.isPopupTrigger()) {
            mouseAction(getCoordsAt(point), BOARD_HEX_POPUP, me.getModifiers());
            return;
        }

        // Disable scrolling when ctrl or alt is held down, since this
        // means the user wants to use the LOS/ruler tools.
        int mask = InputEvent.CTRL_MASK | InputEvent.ALT_MASK;
        if (!GUIPreferences.getInstance().getRightDragScroll()
                && !GUIPreferences.getInstance().getAlwaysRightClickScroll()
                && ((game.getPhase() == IGame.Phase.PHASE_FIRING) || (game.getPhase() == IGame.Phase.PHASE_OFFBOARD))) {
            // In the firing phase, also disable scrolling if
            // the right or middle buttons are clicked, since
            // this means the user wants to activate the
            // popup menu or ruler tool.
            mask |= InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK;
        }

        // disable auto--edge-scrolling if no option set
        if (!GUIPreferences.getInstance().getAutoEdgeScroll()) {
            mask |= InputEvent.BUTTON1_MASK;
        }
        // disable edge-scrolling if no option set
        if (!GUIPreferences.getInstance().getClickEdgeScroll()) {
            mask |= InputEvent.BUTTON3_MASK;
        }

        if (GUIPreferences.getInstance().getRightDragScroll()) {
            mask |= InputEvent.BUTTON2_MASK;
        }

        if ((me.getModifiers() & mask) == 0) {
            isScrolling = true; // activate scrolling
        } else {
            isScrolling = false; // activate scrolling
        }

        if (isTipShowing()) {
            hideTooltip();
        }

        mouseAction(getCoordsAt(point), BOARD_HEX_DRAG, me.getModifiers());
    }

    public void mouseReleased(MouseEvent me) {
        isTipPossible = true;
        oldMousePosition = mousePos;

        for (int i = 0; i < displayables.size(); i++) {
            IDisplayable disp = displayables.get(i);
            if (disp.isReleased()) {
                return;
            }
        }
        isScrolling = false;

        // Unless the user has auto-scroll on and is using the left
        // mouse button, no click action should be triggered if the map
        // is being scrolled.
        if (scrolled
                && (((me.getModifiers() & InputEvent.BUTTON1_MASK) == 0) || !GUIPreferences
                        .getInstance().getAutoEdgeScroll())) {
            return;
        }

        if (me.isPopupTrigger()) {
            mouseAction(getCoordsAt(me.getPoint()), BOARD_HEX_POPUP, me.getModifiers());
            return;
        }

        if (me.getClickCount() == 1) {
            mouseAction(getCoordsAt(me.getPoint()), BOARD_HEX_CLICK, me
                    .getModifiers());
        } else {
            mouseAction(getCoordsAt(me.getPoint()), BOARD_HEX_DOUBLECLICK, me
                    .getModifiers());
        }
    }

    public void mouseEntered(MouseEvent me) {
    }

    public void mouseExited(MouseEvent me) {
        isTipPossible = false;
    }

    public void mouseClicked(MouseEvent me) {
        if (me.isPopupTrigger()) {
            mouseAction(getCoordsAt(me.getPoint()), BOARD_HEX_POPUP, me.getModifiers());
            return;
        }
    }

    //
    // MouseMotionListener
    //
    public void mouseDragged(MouseEvent me) {
        isTipPossible = false;

        Point point = me.getPoint();
        if (null == point) {
            return;
        }

        for (int i = 0; i < displayables.size(); i++) {
            IDisplayable disp = displayables.get(i);
            Point dispPoint = new Point();
            dispPoint.x = point.x - offset.x;
            dispPoint.y = point.y - offset.y;
            if (disp.isDragged(dispPoint, backSize)) {
                repaint();
                return;
            }
        }
        mousePos = point;

        // Disable scrolling when ctrl or alt is held down, since this
        // means the user wants to use the LOS/ruler tools.
        int mask = InputEvent.CTRL_MASK | InputEvent.ALT_MASK;

        if (!GUIPreferences.getInstance().getRightDragScroll()
                && !GUIPreferences.getInstance().getAlwaysRightClickScroll()
                && ((game.getPhase() == IGame.Phase.PHASE_FIRING) || (game.getPhase() == IGame.Phase.PHASE_OFFBOARD))) {
            // In the firing phase, also disable scrolling if
            // the right or middle buttons are clicked, since
            // this means the user wants to activate the
            // popup menu or ruler tool.
            mask |= InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK;
        }

        if (GUIPreferences.getInstance().getRightDragScroll()) {
            mask |= InputEvent.BUTTON1_MASK | InputEvent.BUTTON2_MASK;
        }

        // disable auto--edge-scrolling if no option set
        if (!GUIPreferences.getInstance().getAutoEdgeScroll()) {
            mask |= InputEvent.BUTTON1_MASK;
        }

        // disable edge-scrolling if no option set
        if (!GUIPreferences.getInstance().getClickEdgeScroll()
                && !GUIPreferences.getInstance().getRightDragScroll()) {
            mask |= InputEvent.BUTTON3_MASK;
        }

        if ((me.getModifiers() & mask) == 0) {
            isScrolling = true; // activate scrolling
        } else {
            isScrolling = false; // activate scrolling
        }

        mouseAction(getCoordsAt(point), BOARD_HEX_DRAG, me.getModifiers());
    }

    public void mouseMoved(MouseEvent me) {
        Point point = me.getPoint();
        if (null == point) {
            return;
        }

        for (int i = 0; i < displayables.size(); i++) {
            IDisplayable disp = displayables.get(i);
            if (disp.isBeingDragged()) {
                isTipPossible = false;
                return;
            }
            if (backSize != null) {
                disp.isMouseOver(point, backSize);
            }
        }
        mousePos = point;
        if (isTipShowing()) {
            hideTooltip();
        }
        if (ctlKeyHeld && GUIPreferences.getInstance().getCtlScroll()) {
            if (initCtlScroll) {
                previousMouseX = me.getX();
                previousMouseY = me.getY();
                initCtlScroll = false;
            }
            scroll.x += GUIPreferences.getInstance().getScrollSensitivity()
            * (me.getX() - previousMouseX);
            scroll.y += GUIPreferences.getInstance().getScrollSensitivity()
            * (me.getY() - previousMouseY);
            previousMouseX = me.getX();
            previousMouseY = me.getY();
            checkScrollBounds();
            repaint();
        }
        lastIdle = System.currentTimeMillis();
        isTipPossible = true;
    }

    /**
     * Increases zoomIndex and refreshes the map.
     */
    public void zoomIn() {
        if (zoomIndex == ZOOM_FACTORS.length - 1) {
            return;
        }
        zoomIndex++;
        zoom();
    }

    /**
     * Decreases zoomIndex and refreshes the map.
     */
    public void zoomOut() {
        if (zoomIndex == 0) {
            return;
        }
        zoomIndex--;
        zoom();
    }

    /**
     * zoomIndex is a reference to a static array of scale factors. The index
     * ranges from 0 to 9 and by default is set to 7 which corresponds to a
     * scale of 1.0 (draws megamek images at normal size). To zoom out the index
     * needs to be set to a lower value. To zoom in make it larger. If only
     * zooming a step at a time use the zoomIn and zoomOut methods instead.
     *
     * @param zoomIndex
     */
    public void setZoomIndex(int zoomIndex) {
        this.zoomIndex = zoomIndex;
        zoom();
    }

    public int getZoomIndex() {
        return zoomIndex;
    }

    private void checkZoomIndex() {
        if (zoomIndex > ZOOM_FACTORS.length - 1) {
            zoomIndex = ZOOM_FACTORS.length - 1;
        }
        if (zoomIndex < 0) {
            zoomIndex = 0;
        }
    }

    /**
     * Changes hex dimensions and refreshes the map with the new scale
     */
    private void zoom() {
        checkZoomIndex();
        scale = ZOOM_FACTORS[zoomIndex];
        GUIPreferences.getInstance().setMapZoomIndex(zoomIndex);

        hex_size = new Dimension((int) (HEX_W * scale), (int) (HEX_H * scale));

        final Dimension size = getSize();

        scaledImageCache = new ImageCache<Image, Image>();

        cursorSprite.prepare();
        highlightSprite.prepare();
        selectedSprite.prepare();
        firstLOSSprite.prepare();
        secondLOSSprite.prepare();

        updateBoard();

        view.setLocation(scroll);
        view.setSize(getOptimalView(size));
        offset.setLocation(getOptimalOffset(size));

        updateFontSizes();
        updateBoardImage();

        repaint();
    }

    private void updateFontSizes() {
        if (zoomIndex <= 4) {
            font_elev = FONT_7;
            font_hexnum = FONT_7;
            font_minefield = FONT_7;
        }
        if ((zoomIndex <= 5) & (zoomIndex > 4)) {
            font_elev = FONT_8;
            font_hexnum = FONT_8;
            font_minefield = FONT_8;
        }
        if (zoomIndex > 5) {
            font_elev = FONT_9;
            font_hexnum = FONT_9;
            font_minefield = FONT_9;
        }
    }

    private class MovingUnit {
        public Entity entity;
        public ArrayList<UnitLocation> path;

        MovingUnit(Entity entity, Vector<UnitLocation> path) {
            this.entity = entity;
            this.path = new ArrayList<UnitLocation>(path);
        }
    }

    /**
     * Displays a bit of text in a box. TODO: make multi-line
     */
    private class TooltipCanvas extends Canvas {
        private static final long serialVersionUID = 7085249378990745380L;
        private String[] tipStrings;
        private Dimension size;

        public TooltipCanvas(String[] tipStrings) {
            this.tipStrings = tipStrings;

            // setup
            setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
            setBackground(SystemColor.info);
            setForeground(SystemColor.infoText);

            // determine size
            final FontMetrics fm = getFontMetrics(getFont());
            int width = 0;
            for (String tipString : tipStrings) {
                if (fm.stringWidth(tipString) > width) {
                    width = fm.stringWidth(tipString);
                }
            }
            size = new Dimension(width + 5, fm.getAscent() * tipStrings.length
                    + 4);
            setSize(size);
        }

        @Override
        public void paint(Graphics g) {
            final FontMetrics fm = getFontMetrics(getFont());
            g.setColor(getBackground());
            g.fillRect(0, 0, size.width, size.height);
            g.setColor(getForeground());
            g.drawRect(0, 0, size.width - 1, size.height - 1);
            for (int i = 0; i < tipStrings.length; i++) {
                g.drawString(tipStrings[i], 2, (i + 1) * fm.getAscent());
            }
        }
    }

    /**
     * Everything in the main map view is either the board or it's a sprite
     * displayed on top of the board. Most sprites store a transparent image
     * which they draw onto the screen when told to. Sprites keep a bounds
     * rectangle, so it's easy to tell when they'return onscreen.
     */
    abstract class Sprite implements ImageObserver {
        protected Rectangle bounds;
        protected Image image;

        // Set this to true if you don't want the sprite to be drawn.
        protected boolean hidden = false;

        /**
         * Do any necessary preparation. This is called after creation, but
         * before drawing, when a device context is ready to draw with.
         */
        public abstract void prepare();

        /**
         * When we draw our buffered images, it's necessary to implement the
         * ImageObserver interface. This provides the necesasry functionality.
         */
        public boolean imageUpdate(Image image, int infoflags, int x, int y,
                int width, int height) {
            if (infoflags == ImageObserver.ALLBITS) {
                prepare();
                repaint();
                return false;
            }
            return true;
        }

        /**
         * Returns our bounding rectangle. The coordinates here are stored with
         * the top left corner of the _board_ being 0, 0, so these do not always
         * correspond to screen coordinates.
         */
        public Rectangle getBounds() {
            return bounds;
        }

        /**
         * Are we ready to draw? By default, checks to see that our buffered
         * image has been created.
         */
        public boolean isReady() {
            return image != null;
        }

        /**
         * Draws this sprite onto the specified graphics context.
         */
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            drawOnto(g, x, y, observer, false);
        }

        public void drawOnto(Graphics g, int x, int y, ImageObserver observer,
                boolean makeTranslucent) {
            if (isReady()) {
                if (makeTranslucent) {
                    Graphics2D g2 = (Graphics2D) g;
                    g2.setComposite(AlphaComposite.getInstance(
                            AlphaComposite.SRC_OVER, 0.5f));
                    g2.drawImage(image, x, y, observer);
                    g2.setComposite(AlphaComposite.getInstance(
                            AlphaComposite.SRC_OVER, 1.0f));
                } else {
                    g.drawImage(image, x, y, observer);
                }
            } else {
                // grrr... we'll be ready next time!
                prepare();
            }
        }

        /**
         * Returns true if the point is inside this sprite. Uses board
         * coordinates, not screen coordinates. By default, just checks our
         * bounding rectangle, though some sprites override this for a smaller
         * sensitive area.
         */
        public boolean isInside(Point point) {
            return bounds.contains(point);
        }

        /**
         * Since most sprites being drawn correspond to something in the game,
         * this returns a little info for a tooltip.
         */
        public String[] getTooltip() {
            return null;
        }
    }

    /**
     * Sprite for a cursor. Just a hexagon outline in a specified color.
     */
    private class CursorSprite extends Sprite {
        private Color color;
        private Coords hexLoc;

        public CursorSprite(Color color) {
            this.color = color;
            bounds = new Rectangle(hexPoly.getBounds().width + 1, hexPoly
                    .getBounds().height + 1);
            image = null;

            // start offscreen
            setOffScreen();
        }

        @Override
        public void prepare() {
            // create image for buffer
            Image tempImage = createImage(bounds.width, bounds.height);
            Graphics graph = tempImage.getGraphics();

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);
            // draw attack poly
            graph.setColor(color);
            graph.drawPolygon(hexPoly);

            // create final image
            if (zoomIndex == BASE_ZOOM_INDEX) {
                image = createImage(new FilteredImageSource(tempImage
                        .getSource(), new KeyAlphaFilter(TRANSPARENT)));
            } else {
                image = getScaledImage(createImage(new FilteredImageSource(
                        tempImage.getSource(), new KeyAlphaFilter(TRANSPARENT))));
            }
            graph.dispose();
            tempImage.flush();
        }

        public void setOffScreen() {
            bounds.setLocation(-100, -100);
            hexLoc = new Coords(-2, -2);
        }

        public void setHexLocation(Coords hexLoc) {
            this.hexLoc = hexLoc;
            bounds.setLocation(getHexLocation(hexLoc));
        }

        @Override
        public Rectangle getBounds() {
            bounds = new Rectangle(hexPoly.getBounds().width + 1, hexPoly
                    .getBounds().height + 1);
            bounds.setLocation(getHexLocation(hexLoc));

            return bounds;
        }
    }

    private class GhostEntitySprite extends Sprite {
        private Entity entity;
        private Rectangle modelRect;

        public GhostEntitySprite(Entity entity) {
            this.entity = entity;

            String shortName = entity.getShortName();
            Font font = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
            modelRect = new Rectangle(47, 55, getFontMetrics(font).stringWidth(
                    shortName) + 1, getFontMetrics(font).getAscent());
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));

            bounds = tempBounds;
            image = null;
        }

        /**
         * Creates the sprite for this entity. It is an extra pain to create
         * transparent images in AWT.
         */
        @Override
        public void prepare() {
            // create image for buffer
            Image tempImage;
            Graphics graph;
            try {
                tempImage = createImage(bounds.width, bounds.height);
                graph = tempImage.getGraphics();
            } catch (NullPointerException ex) {
                // argh! but I want it!
                return;
            }

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);

            // draw entity image
            graph.drawImage(tileManager.imageFor(entity), 0, 0, this);

            // create final image
            if (zoomIndex == BASE_ZOOM_INDEX) {
                image = createImage(new FilteredImageSource(tempImage
                        .getSource(), new KeyAlphaFilter(TRANSPARENT)));
            } else {
                image = getScaledImage(createImage(new FilteredImageSource(
                        tempImage.getSource(), new KeyAlphaFilter(TRANSPARENT))));
            }
            graph.dispose();
            tempImage.flush();
        }

        @Override
        public Rectangle getBounds() {
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));
            bounds = tempBounds;

            return bounds;
        }

        @Override
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            drawOnto(g, x, y, observer, true);
        }

    }

    private class MovingEntitySprite extends Sprite {
        private int facing;
        private Entity entity;
        private Rectangle modelRect;

        public MovingEntitySprite(Entity entity, Coords position, int facing) {
            this.entity = entity;
            this.facing = facing;

            String shortName = entity.getShortName();
            Font font = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
            modelRect = new Rectangle(47, 55, getFontMetrics(font).stringWidth(
                    shortName) + 1, getFontMetrics(font).getAscent());
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(position));

            bounds = tempBounds;
            image = null;
        }

        /**
         * Creates the sprite for this entity. It is an extra pain to create
         * transparent images in AWT.
         */
        @Override
        public void prepare() {
            // create image for buffer
            Image tempImage;
            Graphics graph;
            try {
                tempImage = createImage(bounds.width, bounds.height);
                graph = tempImage.getGraphics();
            } catch (NullPointerException ex) {
                // argh! but I want it!
                return;
            }

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);

            // draw entity image
            graph.drawImage(tileManager.imageFor(entity, facing), 0, 0, this);

            // create final image
            if (zoomIndex == BASE_ZOOM_INDEX) {
                image = createImage(new FilteredImageSource(tempImage
                        .getSource(), new KeyAlphaFilter(TRANSPARENT)));
            } else {
                image = getScaledImage(createImage(new FilteredImageSource(
                        tempImage.getSource(), new KeyAlphaFilter(TRANSPARENT))));
            }
            graph.dispose();
            tempImage.flush();
        }
    }

    /**
     * Sprite for an wreck. Consists of an image, drawn from the Tile Manager
     * and an identification label.
     */
    private class WreckSprite extends Sprite {
        private Entity entity;
        private Rectangle modelRect;

        public WreckSprite(Entity entity) {
            this.entity = entity;

            String shortName = entity.getShortName();

            Font font = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
            modelRect = new Rectangle(47, 55, getFontMetrics(font).stringWidth(
                    shortName) + 1, getFontMetrics(font).getAscent());
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));

            bounds = tempBounds;
            image = null;
        }

        @Override
        public Rectangle getBounds() {
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));
            bounds = tempBounds;

            return bounds;
        }

        /**
         * Creates the sprite for this entity. It is an extra pain to create
         * transparent images in AWT.
         */
        @Override
        public void prepare() {
            // figure out size
            String shortName = entity.getShortName();
            Font font = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
            Rectangle tempRect = new Rectangle(47, 55, getFontMetrics(font)
                    .stringWidth(shortName) + 1, getFontMetrics(font)
                    .getAscent());

            // create image for buffer
            Image tempImage;
            Graphics graph;
            try {
                tempImage = createImage(bounds.width, bounds.height);
                graph = tempImage.getGraphics();
            } catch (NullPointerException ex) {
                // argh! but I want it!
                return;
            }

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);

            // Draw wreck image,if we've got one.
            Image wreck = tileManager.wreckMarkerFor(entity);
            if (null != wreck) {
                graph.drawImage(wreck, 0, 0, this);
            }

            // draw box with shortName
            Color text = Color.lightGray;
            Color bkgd = Color.darkGray;
            Color bord = Color.black;

            graph.setFont(font);
            graph.setColor(bord);
            graph.fillRect(tempRect.x, tempRect.y, tempRect.width,
                    tempRect.height);
            tempRect.translate(-1, -1);
            graph.setColor(bkgd);
            graph.fillRect(tempRect.x, tempRect.y, tempRect.width,
                    tempRect.height);
            graph.setColor(text);
            graph.drawString(shortName, tempRect.x + 1, tempRect.y
                    + tempRect.height - 1);

            // create final image
            if (zoomIndex == BASE_ZOOM_INDEX) {
                image = createImage(new FilteredImageSource(tempImage
                        .getSource(), new KeyAlphaFilter(TRANSPARENT)));
            } else {
                image = getScaledImage(createImage(new FilteredImageSource(
                        tempImage.getSource(), new KeyAlphaFilter(TRANSPARENT))));
            }
            graph.dispose();
            tempImage.flush();
        }

        /**
         * Overrides to provide for a smaller sensitive area.
         */
        @Override
        public boolean isInside(Point point) {
            return false;
        }

    }

    /**
     * Sprite for an entity. Changes whenever the entity changes. Consists of an
     * image, drawn from the Tile Manager; facing and possibly secondary facing
     * arrows; armor and internal bars; and an identification label.
     */
    private class EntitySprite extends Sprite {
        private Entity entity;
        private Rectangle entityRect;
        private Rectangle modelRect;

        public EntitySprite(Entity entity) {
            this.entity = entity;

            String shortName = entity.getShortName();

            if (entity.getMovementMode() == IEntityMovementMode.VTOL) {
                shortName = shortName.concat(" (FL: ").concat(
                        Integer.toString(entity.getElevation())).concat(")");
            }
            if (entity.getMovementMode() == IEntityMovementMode.SUBMARINE) {
                shortName = shortName.concat(" (Depth: ").concat(
                        Integer.toString(entity.getElevation())).concat(")");
            }
            int face = entity.isCommander() ? Font.ITALIC : Font.PLAIN;
            Font font = new Font("SansSerif", face, 10); //$NON-NLS-1$
            modelRect = new Rectangle(47, 55, getFontMetrics(font).stringWidth(
                    shortName) + 1, getFontMetrics(font).getAscent());
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));

            bounds = tempBounds;
            entityRect = new Rectangle(bounds.x + (int) (20 * scale),
                    bounds.y + (int) (14 * scale), (int) (44 * scale),
                    (int) (44 * scale));
            image = null;
        }

        @Override
        public Rectangle getBounds() {
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));
            bounds = tempBounds;

            entityRect = new Rectangle(bounds.x + (int) (20 * scale),
                    bounds.y + (int) (14 * scale), (int) (44 * scale),
                    (int) (44 * scale));

            return bounds;
        }

        @Override
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            if (trackThisEntitiesVisibilityInfo(entity)
                    && !entity.isVisibleToEnemy()
                    && GUIPreferences.getInstance().getBoolean(
                            GUIPreferences.ADVANCED_TRANSLUCENT_HIDDEN_UNITS)) {
                // create final image with translucency
                drawOnto(g, x, y, observer, true);
            } else {
                drawOnto(g, x, y, observer, false);
            }
        }

        /**
         * Creates the sprite for this entity. It is an extra pain to create
         * transparent images in AWT.
         */
        @Override
        public void prepare() {
            // figure out size
            String shortName = entity.getShortName();
            if (entity.getMovementMode() == IEntityMovementMode.VTOL) {
                shortName = shortName.concat(" (FL: ").concat(
                        Integer.toString(entity.getElevation())).concat(")");
            }
            if (PreferenceManager.getClientPreferences().getShowUnitId()) {
                shortName += (Messages.getString("BoardView1.ID") + entity.getId()); //$NON-NLS-1$
            }
            int face = entity.isCommander() ? Font.ITALIC : Font.PLAIN;
            Font font = new Font("SansSerif", face, 10); //$NON-NLS-1$
            Rectangle tempRect = new Rectangle(47, 55, getFontMetrics(font)
                    .stringWidth(shortName) + 1, getFontMetrics(font)
                    .getAscent());

            // create image for buffer
            Image tempImage;
            Graphics graph;
            try {
                tempImage = createImage(bounds.width, bounds.height);
                graph = tempImage.getGraphics();
            } catch (NullPointerException ex) {
                // argh! but I want it!
                return;
            }

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);

            // draw entity image
            graph.drawImage(tileManager.imageFor(entity), 0, 0, this);

            // draw box with shortName
            Color text, bkgd, bord;
            if (entity.isDone()) {
                text = Color.lightGray;
                bkgd = Color.darkGray;
                bord = Color.black;
            } else if (entity.isImmobile()) {
                text = Color.darkGray;
                bkgd = Color.black;
                bord = Color.lightGray;
            } else {
                text = Color.black;
                bkgd = Color.lightGray;
                bord = Color.darkGray;
            }
            graph.setFont(font);
            graph.setColor(bord);
            graph.fillRect(tempRect.x, tempRect.y, tempRect.width,
                    tempRect.height);
            tempRect.translate(-1, -1);
            graph.setColor(bkgd);
            graph.fillRect(tempRect.x, tempRect.y, tempRect.width,
                    tempRect.height);
            graph.setColor(text);
            graph.drawString(shortName, tempRect.x + 1, tempRect.y
                    + tempRect.height - 1);

            // draw facing
            graph.setColor(Color.white);
            if ((entity.getFacing() != -1)
                    && !((entity instanceof Infantry) && (((Infantry) entity)
                            .getDugIn() == Infantry.DUG_IN_NONE))
                            && !((entity instanceof Aero) && ((Aero)entity).isSpheroid()
                                    && game.getBoard().inAtmosphere())) {
                graph.drawPolygon(facingPolys[entity.getFacing()]);
            }

            // determine secondary facing for non-mechs & flipped arms
            int secFacing = entity.getFacing();
            if (!((entity instanceof Mech) || (entity instanceof Protomech))) {
                secFacing = entity.getSecondaryFacing();
            } else if (entity.getArmsFlipped()) {
                secFacing = (entity.getFacing() + 3) % 6;
            }
            // draw red secondary facing arrow if necessary
            if ((secFacing != -1) && (secFacing != entity.getFacing())) {
                graph.setColor(Color.red);
                graph.drawPolygon(facingPolys[secFacing]);
            }

            // Determine if the entity has a locked turret,
            // and if it is a gun emplacement
            boolean turretLocked = false;
            int crewStunned = 0;
            boolean ge = false;
            if (entity instanceof Tank) {
                turretLocked = !((Tank) entity).hasNoTurret()
                && !entity.canChangeSecondaryFacing();
                crewStunned = ((Tank) entity).getStunnedTurns();
            } else if (entity instanceof GunEmplacement) {
                turretLocked = ((GunEmplacement) entity).hasTurret()
                && !entity.canChangeSecondaryFacing();
                ge = true;
            }

            // draw condition strings
            if(entity instanceof Aero) {
                Aero a = (Aero)entity;

                //                draw altitude if Aero in atmosphere
                if(game.getBoard().inAtmosphere()) {
                    graph.setColor(Color.darkGray);
                    graph.drawString(Integer.toString(a.getElevation()), 26, 15);
                    graph.setColor(Color.PINK);
                    graph.drawString(Integer.toString(a.getElevation()), 25, 14);
                }

                if(a.isRolled()) {
                    // draw "rolled"
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.ROLLED"), 18, 15); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.ROLLED"), 17, 14); //$NON-NLS-1$
                }

                if(a.isOutControlTotal() & a.isRandomMove()) {
                    // draw "RANDOM"
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.RANDOM"), 18, 35); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.RANDOM"), 17, 34); //$NON-NLS-1$
                } else if (a.isOutControlTotal()) {
                    //                     draw "CONTROL"
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.CONTROL"), 18, 39); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.CONTROL"), 17, 38); //$NON-NLS-1$
                }

                if(a.isEvading()) {
                    //draw "EVADE" - can't overlap with out of control
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.EVADE"), 18, 39); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.EVADE"), 17, 38); //$NON-NLS-1$
                }
            }

            if (entity.crew.isDead()) {
                // draw "CREW DEAD"
                graph.setColor(Color.darkGray);
                graph.drawString(
                        Messages.getString("BoardView1.CrewDead"), 18, 39); //$NON-NLS-1$
                graph.setColor(Color.red);
                graph.drawString(
                        Messages.getString("BoardView1.CrewDead"), 17, 38); //$NON-NLS-1$
            } else if (!ge && entity.isImmobile()) {
                if (entity.isProne()) {
                    // draw "IMMOBILE" and "PRONE"
                    graph.setColor(Color.darkGray);
                    graph.drawString(
                            Messages.getString("BoardView1.IMMOBILE"), 18, 35); //$NON-NLS-1$
                    graph.drawString(
                            Messages.getString("BoardView1.PRONE"), 26, 48); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(
                            Messages.getString("BoardView1.IMMOBILE"), 17, 34); //$NON-NLS-1$
                    graph.setColor(Color.yellow);
                    graph.drawString(
                            Messages.getString("BoardView1.PRONE"), 25, 47); //$NON-NLS-1$
                } else if (crewStunned > 0) {
                    // draw IMMOBILE and STUNNED
                    graph.setColor(Color.darkGray);
                    graph.drawString(
                            Messages.getString("BoardView1.IMMOBILE"), 18, 35); //$NON-NLS-1$
                    graph
                    .drawString(
                            Messages
                            .getString(
                                    "BoardView1.STUNNED", new Object[] { crewStunned }), 22, 48); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(
                            Messages.getString("BoardView1.IMMOBILE"), 17, 34); //$NON-NLS-1$
                    graph.setColor(Color.yellow);
                    graph
                    .drawString(
                            Messages
                            .getString(
                                    "BoardView1.STUNNED", new Object[] { crewStunned }), 21, 47); //$NON-NLS-1$
                } else if (turretLocked) {
                    // draw "IMMOBILE" and "LOCKED"
                    graph.setColor(Color.darkGray);
                    graph.drawString(
                            Messages.getString("BoardView1.IMMOBILE"), 18, 35); //$NON-NLS-1$
                    graph.drawString(
                            Messages.getString("BoardView1.LOCKED"), 22, 48); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(
                            Messages.getString("BoardView1.IMMOBILE"), 17, 34); //$NON-NLS-1$
                    graph.setColor(Color.yellow);
                    graph.drawString(
                            Messages.getString("BoardView1.LOCKED"), 21, 47); //$NON-NLS-1$
                } else {
                    // draw "IMMOBILE"
                    graph.setColor(Color.darkGray);
                    graph.drawString(
                            Messages.getString("BoardView1.IMMOBILE"), 18, 39); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(
                            Messages.getString("BoardView1.IMMOBILE"), 17, 38); //$NON-NLS-1$
                }
            } else if (entity.isProne()) {
                // draw "PRONE"
                graph.setColor(Color.darkGray);
                graph.drawString(
                        Messages.getString("BoardView1.PRONE"), 26, 39); //$NON-NLS-1$
                graph.setColor(Color.yellow);
                graph.drawString(
                        Messages.getString("BoardView1.PRONE"), 25, 38); //$NON-NLS-1$
            } else if (crewStunned > 0) {
                // draw STUNNED
                graph.setColor(Color.darkGray);
                graph.drawString(
                        Messages
                        .getString(
                                "BoardView1.STUNNED", new Object[] { crewStunned }), 22, 48); //$NON-NLS-1$
                graph.setColor(Color.yellow);
                graph.drawString(
                        Messages
                        .getString(
                                "BoardView1.STUNNED", new Object[] { crewStunned }), 21, 47); //$NON-NLS-1$
            } else if (turretLocked) {
                // draw "LOCKED"
                graph.setColor(Color.darkGray);
                graph.drawString(
                        Messages.getString("BoardView1.LOCKED"), 22, 39); //$NON-NLS-1$
                graph.setColor(Color.yellow);
                graph.drawString(
                        Messages.getString("BoardView1.LOCKED"), 21, 38); //$NON-NLS-1$
            }

            // If this unit is being swarmed or is swarming another, say so.
            if (Entity.NONE != entity.getSwarmAttackerId()) {
                // draw "SWARMED"
                graph.setColor(Color.darkGray);
                graph.drawString(
                        Messages.getString("BoardView1.SWARMED"), 17, 22); //$NON-NLS-1$
                graph.setColor(Color.red);
                graph.drawString(
                        Messages.getString("BoardView1.SWARMED"), 16, 21); //$NON-NLS-1$
            }

            // If this unit is transporting another, say so.
            if ((entity.getLoadedUnits()).size() > 0) {
                // draw "T"
                graph.setColor(Color.darkGray);
                graph.drawString("T", 20, 71); //$NON-NLS-1$
                graph.setColor(Color.black);
                graph.drawString("T", 19, 70); //$NON-NLS-1$
            }

            // If this unit is stuck, say so.
            if ((entity.isStuck())) {
                graph.setColor(Color.darkGray);
                graph
                .drawString(
                        Messages.getString("BoardView1.STUCK"), 26, 61); //$NON-NLS-1$
                graph.setColor(Color.orange);
                graph
                .drawString(
                        Messages.getString("BoardView1.STUCK"), 25, 60); //$NON-NLS-1$

            }

            // If this unit is currently unknown to the enemy, say so.
            if (trackThisEntitiesVisibilityInfo(entity)) {
                if (!entity.isSeenByEnemy()) {
                    // draw "U"
                    graph.setColor(Color.darkGray);
                    graph.drawString("U", 30, 71); //$NON-NLS-1$
                    graph.setColor(Color.black);
                    graph.drawString("U", 29, 70); //$NON-NLS-1$
                } else if (!entity.isVisibleToEnemy()
                        && !GUIPreferences
                        .getInstance()
                        .getBoolean(
                                GUIPreferences.ADVANCED_TRANSLUCENT_HIDDEN_UNITS)) {
                    // If this unit is currently hidden from the enemy, say so.
                    // draw "H"
                    graph.setColor(Color.darkGray);
                    graph.drawString("H", 30, 71); //$NON-NLS-1$
                    graph.setColor(Color.black);
                    graph.drawString("H", 29, 70); //$NON-NLS-1$
                }
            }

            // If hull down, show
            if (entity.isHullDown()) {
                // draw "D"
                graph.setColor(Color.darkGray);
                graph.drawString(Messages.getString("UnitOverview.HULLDOWN"), 15, 39); //$NON-NLS-1$
                graph.setColor(Color.yellow);
                graph.drawString(Messages.getString("UnitOverview.HULLDOWN"), 14, 38); //$NON-NLS-1$
            } else if (entity instanceof Infantry) {
                int dig = ((Infantry) entity).getDugIn();
                if (dig == Infantry.DUG_IN_COMPLETE) {
                    // draw "D"
                    graph.setColor(Color.darkGray);
                    graph.drawString("D", 40, 71); //$NON-NLS-1$
                    graph.setColor(Color.black);
                    graph.drawString("D", 39, 70); //$NON-NLS-1$
                } else if (dig != Infantry.DUG_IN_NONE) {
                    // draw "W"
                    graph.setColor(Color.darkGray);
                    graph.drawString("W", 40, 71); //$NON-NLS-1$
                    graph.setColor(Color.black);
                    graph.drawString("W", 39, 70); //$NON-NLS-1$
                }
            }

            // Lets draw our armor and internal status bars
            int baseBarLength = 23;
            int barLength = 0;
            double percentRemaining = 0.00;

            percentRemaining = entity.getArmorRemainingPercent();
            barLength = (int) (baseBarLength * percentRemaining);

            graph.setColor(Color.darkGray);
            graph.fillRect(56, 7, 23, 3);
            graph.setColor(Color.lightGray);
            graph.fillRect(55, 6, 23, 3);
            graph.setColor(getStatusBarColor(percentRemaining));
            graph.fillRect(55, 6, barLength, 3);

            if (!ge) {
                // Gun emplacements don't have internal structure
                percentRemaining = entity.getInternalRemainingPercent();
                barLength = (int) (baseBarLength * percentRemaining);

                graph.setColor(Color.darkGray);
                graph.fillRect(56, 11, 23, 3);
                graph.setColor(Color.lightGray);
                graph.fillRect(55, 10, 23, 3);
                graph.setColor(getStatusBarColor(percentRemaining));
                graph.fillRect(55, 10, barLength, 3);
            }

            // create final image
            if (zoomIndex == BASE_ZOOM_INDEX) {
                image = createImage(new FilteredImageSource(tempImage
                        .getSource(), new KeyAlphaFilter(TRANSPARENT)));
            } else {
                image = getScaledImage(createImage(new FilteredImageSource(
                        tempImage.getSource(), new KeyAlphaFilter(TRANSPARENT))));
            }
            graph.dispose();
            tempImage.flush();
        }

        /**
         * We only want to show double-blind visibility indicators on our own
         * mechs and teammates mechs (assuming team vision option).
         */
        private boolean trackThisEntitiesVisibilityInfo(Entity e) {
            if (getLocalPlayer() == null) {
                return false;
            }

            if (game.getOptions().booleanOption("double_blind") //$NON-NLS-1$
                    && ((e.getOwner().getId() == getLocalPlayer().getId()) || (game
                            .getOptions().booleanOption("team_vision") //$NON-NLS-1$
                            && (e.getOwner().getTeam() == getLocalPlayer().getTeam())))) {
                return true;
            }
            return false;
        }

        private Color getStatusBarColor(double percentRemaining) {
            if (percentRemaining <= .25) {
                return Color.red;
            } else if (percentRemaining <= .75) {
                return Color.yellow;
            } else {
                return new Color(16, 196, 16);
            }
        }

        /**
         * Overrides to provide for a smaller sensitive area.
         */
        @Override
        public boolean isInside(Point point) {
            return entityRect.contains(point.x + view.x - offset.x, point.y
                    + view.y - offset.y);
        }

        @Override
        public String[] getTooltip() {
            String[] tipStrings = new String[3];
            StringBuffer buffer;

            buffer = new StringBuffer();
            buffer.append(entity.getChassis()).append(" (") //$NON-NLS-1$
            .append(entity.getOwner().getName()).append("); ") //$NON-NLS-1$
            .append(entity.getCrew().getGunnery()).append("/") //$NON-NLS-1$
            .append(entity.getCrew().getPiloting()).append(
                    Messages.getString("BoardView1.pilot")); //$NON-NLS-1$
            int numAdv = entity.getCrew().countAdvantages();
            boolean isMD = entity.getCrew().countMDImplants() > 0;
            if (numAdv > 0) {
                buffer.append(" <") //$NON-NLS-1$
                .append(numAdv).append(
                        Messages.getString("BoardView1.advs")); //$NON-NLS-1$
            }
            if (isMD) {
                buffer.append(Messages.getString("BoardView1.md")); //$NON-NLS-1$
            }
            tipStrings[0] = buffer.toString();

            GunEmplacement ge = null;
            if (entity instanceof GunEmplacement) {
                ge = (GunEmplacement) entity;
            }

            Aero a = null;
            if(entity instanceof Aero) {
                a = (Aero) entity;
            }

            buffer = new StringBuffer();
            if ((ge == null) && (a == null)) {
                buffer.append(Messages.getString("BoardView1.move")) //$NON-NLS-1$
                .append(entity.getMovementAbbr(entity.moved)).append(
                ":") //$NON-NLS-1$
                .append(entity.delta_distance).append(" (+") //$NON-NLS-1$
                .append(
                        Compute.getTargetMovementModifier(game,
                                entity.getId()).getValue())
                                .append(");") //$NON-NLS-1$
                                .append(Messages.getString("BoardView1.Heat")) //$NON-NLS-1$
                                .append(entity.heat);
                if (entity.isCharging()) {
                    buffer.append(" ") //$NON-NLS-1$
                    .append(Messages.getString("BoardView1.charge1")); //$NON-NLS-1$
                }
                if (entity.isMakingDfa()) {
                    buffer.append(" ") //$NON-NLS-1$
                    .append(Messages.getString("BoardView1.DFA1")); //$NON-NLS-1$
                }
            } else if (ge == null) {
                buffer.append( Messages.getString("BoardView1.move") ) //$NON-NLS-1$
                .append( entity.getMovementAbbr(entity.moved) )
                .append( ":" ) //$NON-NLS-1$
                .append( entity.delta_distance )
                .append( Messages.getString("BoardView1.Heat") ) //$NON-NLS-1$
                .append( entity.heat );
                if (entity.isCharging()) {
                    buffer.append(" ") //$NON-NLS-1$
                    .append( Messages.getString("BoardView1.charge1")); //$NON-NLS-1$
                }
            } else {
                if (ge.hasTurret() && ge.isTurretLocked()) {
                    buffer
                    .append(Messages
                            .getString("BoardView1.TurretLocked"));
                    if (ge.getFirstWeapon() == -1) {
                        buffer.append(",");
                        buffer.append(Messages
                                .getString("BoardView1.WeaponsDestroyed"));
                    }
                } else if (ge.getFirstWeapon() == -1) {
                    buffer.append(Messages
                            .getString("BoardView1.WeaponsDestroyed"));
                } else {
                    buffer.append(Messages.getString("BoardView1.Operational"));
                }
            }
            if (entity.isDone()) {
                buffer.append(" (").append(
                        Messages.getString("BoardView1.done")).append(")");
            }
            tipStrings[1] = buffer.toString();

            buffer = new StringBuffer();
            if (ge == null) {
                buffer.append(Messages.getString("BoardView1.Armor")) //$NON-NLS-1$
                .append(entity.getTotalArmor()).append(
                        Messages.getString("BoardView1.internal")) //$NON-NLS-1$
                        .append(entity.getTotalInternal());
            } else {
                buffer.append(Messages.getString("BoardView1.cf")) //$NON-NLS-1$
                .append(ge.getCurrentCF()).append(
                        Messages.getString("BoardView1.turretArmor")) //$NON-NLS-1$
                        .append(ge.getCurrentTurretArmor());
            }
            tipStrings[2] = buffer.toString();

            return tipStrings;
        }
    }

    /**
     * Sprite for a step in a movement path. Only one sprite should exist for
     * any hex in a path. Contains a colored number, and arrows indicating
     * entering, exiting or turning.
     */
    private class StepSprite extends Sprite {
        private MoveStep step;

        public StepSprite(MoveStep step) {
            this.step = step;

            // step is the size of the hex that this step is in
            bounds = new Rectangle(getHexLocation(step.getPosition()), hex_size);
            image = null;
        }

        @Override
        public void prepare() {
            // create image for buffer
            Image tempImage = createImage(bounds.width, bounds.height);
            Graphics graph = tempImage.getGraphics();

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);

            // setup some variables
            final Point stepPos = getHexLocation(step.getPosition());
            stepPos.translate(-bounds.x, -bounds.y);
            final Polygon facingPoly = facingPolys[step.getFacing()];
            final Polygon movePoly = movementPolys[step.getFacing()];
            Point offsetCostPos;
            Polygon myPoly;
            Color col;
            // set color
            switch (step.getMovementType()) {
            case IEntityMovementType.MOVE_RUN:
            case IEntityMovementType.MOVE_VTOL_RUN:
            case IEntityMovementType.MOVE_OVER_THRUST:
                if (step.isUsingMASC()) {
                    col = GUIPreferences.getInstance().getColor(
                            "AdvancedMoveMASCColor");
                } else {
                    col = GUIPreferences.getInstance().getColor(
                            "AdvancedMoveRunColor");
                }
                break;
            case IEntityMovementType.MOVE_JUMP:
                col = GUIPreferences.getInstance().getColor(
                        "AdvancedMoveJumpColor");
                break;
            case IEntityMovementType.MOVE_ILLEGAL:
                col = GUIPreferences.getInstance().getColor(
                        "AdvancedMoveIllegalColor");
                break;
            default:
                if (step.getType() == MovePath.STEP_BACKWARDS) {
                    col = GUIPreferences.getInstance().getColor(
                            "AdvancedMoveBackColor");
                } else {
                    col = GUIPreferences.getInstance().getColor(
                            "AdvancedMoveDefaultColor");
                }
            break;
            }

            if(game.useVectorMove()) {
                drawActiveVectors(step, stepPos, graph);
            }

            drawConditions(step, stepPos, graph, col);

            // draw arrows and cost for the step
            switch (step.getType()) {
            case MovePath.STEP_FORWARDS:
            case MovePath.STEP_SWIM:
            case MovePath.STEP_BACKWARDS:
            case MovePath.STEP_CHARGE:
            case MovePath.STEP_DFA:
            case MovePath.STEP_LATERAL_LEFT:
            case MovePath.STEP_LATERAL_RIGHT:
            case MovePath.STEP_LATERAL_LEFT_BACKWARDS:
            case MovePath.STEP_LATERAL_RIGHT_BACKWARDS:
            case MovePath.STEP_DEC:
            case MovePath.STEP_DECN:
            case MovePath.STEP_ACC:
            case MovePath.STEP_ACCN:
            case MovePath.STEP_LOOP:
                // draw arrows showing them entering the next
                myPoly = new Polygon(movePoly.xpoints, movePoly.ypoints,
                        movePoly.npoints);
                graph.setColor(Color.darkGray);
                myPoly.translate(stepPos.x + 1, stepPos.y + 1);
                graph.drawPolygon(myPoly);
                graph.setColor(col);
                myPoly.translate(-1, -1);
                graph.drawPolygon(myPoly);
                // draw movement cost
                drawMovementCost(step, stepPos, graph, col, true);
                drawRemainingVelocity(step, stepPos, graph, true);
                break;
            case MovePath.STEP_GO_PRONE:
            case MovePath.STEP_HULL_DOWN:
            case MovePath.STEP_DOWN:
            case MovePath.STEP_DIG_IN:
            case MovePath.STEP_FORTIFY:
                //draw arrow indicating dropping prone
                // also doubles as the descent indication
                Polygon downPoly = movementPolys[7];
                myPoly = new Polygon(downPoly.xpoints, downPoly.ypoints,
                        downPoly.npoints);
                graph.setColor(Color.darkGray);
                myPoly.translate(stepPos.x, stepPos.y);
                graph.drawPolygon(myPoly);
                graph.setColor(col);
                myPoly.translate(-1, -1);
                graph.drawPolygon(myPoly);
                offsetCostPos = new Point(stepPos.x + 1, stepPos.y + 15);
                drawMovementCost(step, offsetCostPos, graph, col, false);
                drawRemainingVelocity(step, stepPos, graph, true);
                break;
            case MovePath.STEP_GET_UP:
            case MovePath.STEP_UP:
            case MovePath.STEP_CAREFUL_STAND:
                // draw arrow indicating standing up
                // also doubles as the climb indication
                // and triples as deceleration
                Polygon upPoly = movementPolys[6];
                myPoly = new Polygon(upPoly.xpoints, upPoly.ypoints,
                        upPoly.npoints);
                graph.setColor(Color.darkGray);
                myPoly.translate(stepPos.x, stepPos.y);
                graph.drawPolygon(myPoly);
                graph.setColor(col);
                myPoly.translate(-1, -1);
                graph.drawPolygon(myPoly);
                offsetCostPos = new Point(stepPos.x, stepPos.y + 15);
                drawMovementCost(step, offsetCostPos, graph, col, false);
                drawRemainingVelocity(step, stepPos, graph, true);
                break;
            case MovePath.STEP_CLIMB_MODE_ON:
                // draw climb mode indicator
                String climb;
                if (step.getParent().getEntity().getMovementMode() == IEntityMovementMode.WIGE) {
                    climb = Messages.getString("BoardView1.WIGEClimb"); //$NON-NLS-1$
                } else {
                    climb = Messages.getString("BoardView1.Climb"); //$NON-NLS-1$
                }
                if (step.isPastDanger()) {
                    climb = "(" + climb + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int climbX = stepPos.x
                + 42
                - (graph.getFontMetrics(graph.getFont())
                        .stringWidth(climb) / 2);
                graph.setColor(Color.darkGray);
                graph.drawString(climb, climbX, stepPos.y + 39);
                graph.setColor(col);
                graph.drawString(climb, climbX - 1, stepPos.y + 38);
                break;
            case MovePath.STEP_CLIMB_MODE_OFF:
                // cancel climb mode indicator
                String climboff;
                if (step.getParent().getEntity().getMovementMode() == IEntityMovementMode.WIGE) {
                    climboff = Messages.getString("BoardView1.WIGEClimbOff"); //$NON-NLS-1$
                } else {
                    climboff = Messages.getString("BoardView1.ClimbOff"); //$NON-NLS-1$
                }
                if (step.isPastDanger()) {
                    climboff = "(" + climboff + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int climboffX = stepPos.x
                + 42
                - (graph.getFontMetrics(graph.getFont())
                        .stringWidth(climboff) / 2);
                graph.setColor(Color.darkGray);
                graph.drawString(climboff, climboffX, stepPos.y + 39);
                graph.setColor(col);
                graph.drawString(climboff, climboffX - 1, stepPos.y + 38);
                break;
            case MovePath.STEP_TURN_LEFT:
            case MovePath.STEP_TURN_RIGHT:
            case MovePath.STEP_THRUST:
            case MovePath.STEP_YAW:
            case MovePath.STEP_EVADE:
            case MovePath.STEP_ROLL:
                // draw arrows showing the facing
                myPoly = new Polygon(facingPoly.xpoints,
                        facingPoly.ypoints, facingPoly.npoints);
                graph.setColor(Color.darkGray);
                myPoly.translate(stepPos.x + 1, stepPos.y + 1);
                graph.drawPolygon(myPoly);
                graph.setColor(col);
                myPoly.translate(-1, -1);
                graph.drawPolygon(myPoly);
                if(game.useVectorMove()) {
                    drawMovementCost(step, stepPos, graph, col, false);
                }
                break;
            case MovePath.STEP_LOAD:
                // Announce load.
                String load = Messages.getString("BoardView1.Load"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    load = "(" + load + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int loadX = stepPos.x
                + 42
                - (graph.getFontMetrics(graph.getFont())
                        .stringWidth(load) / 2);
                graph.setColor(Color.darkGray);
                graph.drawString(load, loadX, stepPos.y + 39);
                graph.setColor(col);
                graph.drawString(load, loadX - 1, stepPos.y + 38);
                break;
            case MovePath.STEP_LAUNCH:
                //announce launch
                String launch = Messages.getString("BoardView1.Launch"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    launch = "(" + launch + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int launchX = stepPos.x + 42 - (graph.getFontMetrics(graph.getFont()).stringWidth(launch) / 2);
                int launchY = stepPos.y + 38 + graph.getFontMetrics(graph.getFont()).getHeight();
                graph.setColor(Color.darkGray);
                graph.drawString(launch, launchX, launchY + 1);
                graph.setColor(col);
                graph.drawString(launch, launchX - 1, launchY);
                break;
            case MovePath.STEP_RECOVER:
                //announce launch
                String recover = Messages.getString("BoardView1.Recover"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    launch = "(" + recover + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int recoverX = stepPos.x + 42 - (graph.getFontMetrics(graph.getFont()).stringWidth(recover) / 2);
                int recoverY = stepPos.y + 38 + graph.getFontMetrics(graph.getFont()).getHeight();
                graph.setColor(Color.darkGray);
                graph.drawString(recover, recoverX, recoverY + 1);
                graph.setColor(col);
                graph.drawString(recover, recoverX - 1, recoverY);
                break;
            case MovePath.STEP_JOIN:
                //announce launch
                String join = Messages.getString("BoardView1.Join"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    launch = "(" + join + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int joinX = stepPos.x + 42 - (graph.getFontMetrics(graph.getFont()).stringWidth(join) / 2);
                int joinY = stepPos.y + 38 + graph.getFontMetrics(graph.getFont()).getHeight();
                graph.setColor(Color.darkGray);
                graph.drawString(join, joinX, joinY + 1);
                graph.setColor(col);
                graph.drawString(join, joinX - 1, joinY);
                break;
            case MovePath.STEP_UNLOAD:
                // Announce unload.
                String unload = Messages.getString("BoardView1.Unload"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    unload = "(" + unload + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int unloadX = stepPos.x
                + 42
                - (graph.getFontMetrics(graph.getFont())
                        .stringWidth(unload) / 2);
                int unloadY = stepPos.y + 38
                + graph.getFontMetrics(graph.getFont()).getHeight();
                graph.setColor(Color.darkGray);
                graph.drawString(unload, unloadX, unloadY + 1);
                graph.setColor(col);
                graph.drawString(unload, unloadX - 1, unloadY);
                break;
            case MovePath.STEP_HOVER:
                //announce launch
                String hover = Messages.getString("BoardView1.Hover"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    hover = "(" + hover + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int hoverX = stepPos.x + 42 - (graph.getFontMetrics(graph.getFont()).stringWidth(hover) / 2);
                int hoverY = stepPos.y + 38 + graph.getFontMetrics(graph.getFont()).getHeight();
                graph.setColor(Color.darkGray);
                graph.drawString(hover, hoverX, hoverY + 1);
                graph.setColor(col);
                graph.drawString(hover, hoverX - 1, hoverY);
                drawMovementCost(step, stepPos, graph, col, false);
                break;
            default:
                break;
            }

            // create final image
            if (zoomIndex == BASE_ZOOM_INDEX) {
                image = createImage(new FilteredImageSource(tempImage
                        .getSource(), new KeyAlphaFilter(TRANSPARENT)));
            } else {
                image = getScaledImage(createImage(new FilteredImageSource(
                        tempImage.getSource(), new KeyAlphaFilter(TRANSPARENT))));
            }
            graph.dispose();
            tempImage.flush();
        }

        /**
         * draw conditions separate from the step, This allows me to keep
         * conditions on the Aero even when that step is erased (as per
         * advanced movement). For now, just evading and rolling. eventually
         * loading and unloading as well
         */
        private void drawConditions(MoveStep step, Point stepPos, Graphics graph, Color col) {
            if(step.isEvading()) {
                String evade = Messages.getString("BoardView1.Evade"); //$NON-NLS-1$
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int evadeX = stepPos.x + 42 - (graph.getFontMetrics(graph.getFont()).stringWidth(evade) / 2);
                graph.setColor(Color.darkGray);
                graph.drawString(evade, evadeX, stepPos.y + 64);
                graph.setColor(col);
                graph.drawString(evade, evadeX - 1, stepPos.y + 63);
            }

            if(step.isRolled()) {
                //Announce roll
                String roll = Messages.getString("BoardView1.Roll"); //$NON-NLS-1$
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int rollX = stepPos.x + 42 - (graph.getFontMetrics(graph.getFont()).stringWidth(roll) / 2);
                graph.setColor(Color.darkGray);
                graph.drawString(roll, rollX, stepPos.y + 18);
                graph.setColor(col);
                graph.drawString(roll, rollX - 1, stepPos.y + 17);
            }


        }

        private void drawActiveVectors(MoveStep step, Point stepPos, Graphics graph) {

            /*TODO: it might be better to move this to the MovementSprite
             * so that it is visible before first step and you can't see it
             * for all entities
             */

            int[] activeXpos = {39, 59, 59, 40, 19, 19};
            int[] activeYpos = {20, 28, 52, 59, 52, 28};

            int[] v = step.getVectors();
            for(int i = 0; i < 6; i++) {

                String active = Integer.toString(v[i]);
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                graph.setColor(Color.darkGray);
                graph.drawString(active, activeXpos[i] + stepPos.x, activeYpos[i] + stepPos.y);
                graph.setColor(Color.red);
                graph.drawString(active, activeXpos[i] + stepPos.x - 1, activeYpos[i] + stepPos.y - 1);

            }

        }

        @Override
        public Rectangle getBounds() {
            bounds = new Rectangle(getHexLocation(step.getPosition()), hex_size);
            return bounds;
        }

        public MoveStep getStep() {
            return step;
        }

        private void drawRemainingVelocity(MoveStep step, Point stepPos,
                Graphics graph, boolean shiftFlag) {
            String velString = null;
            StringBuffer velStringBuf = new StringBuffer();

            if (!game.useVectorMove() &&
                    ((step.getMovementType() == IEntityMovementType.MOVE_SAFE_THRUST)
                            || (step.getMovementType() == IEntityMovementType.MOVE_OVER_THRUST))) {
                velStringBuf.append("(")
                .append(step.getVelocityLeft())
                .append("/")
                .append(step.getVelocity())
                .append(")");
            }

            Color col = Color.GREEN;
            if(step.getVelocityLeft() > 0) {
                col = Color.RED;
            }

            //          Convert the buffer to a String and draw it.
            velString = velStringBuf.toString();
            graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
            int costX = stepPos.x + 42;
            if (shiftFlag) {
                costX -= (graph.getFontMetrics(graph.getFont()).stringWidth(
                        velString) / 2);
            }
            graph.setColor(Color.darkGray);
            graph.drawString(velString, costX, stepPos.y + 28);
            graph.setColor(col);
            graph.drawString(velString, costX - 1, stepPos.y + 27);

        }

        private void drawMovementCost(MoveStep step, Point stepPos,
                Graphics graph, Color col, boolean shiftFlag) {
            String costString = null;
            StringBuffer costStringBuf = new StringBuffer();
            costStringBuf.append(step.getMpUsed());

            // If the step is using a road bonus, mark it.
            if (step.isOnlyPavement()
                    && (step.getParent().getEntity() instanceof Tank)) {
                costStringBuf.append("+"); //$NON-NLS-1$
            }

            // If the step is dangerous, mark it.
            if (step.isDanger()) {
                costStringBuf.append("*"); //$NON-NLS-1$
            }

            // If the step is past danger, mark that.
            if (step.isPastDanger()) {
                costStringBuf.insert(0, "("); //$NON-NLS-1$
                costStringBuf.append(")"); //$NON-NLS-1$
            }

            if (step.isUsingMASC()) {
                costStringBuf.append("["); //$NON-NLS-1$
                costStringBuf.append(step.getTargetNumberMASC());
                costStringBuf.append("+]"); //$NON-NLS-1$
            }

            if ((step.getMovementType() == IEntityMovementType.MOVE_VTOL_WALK)
                    || (step.getMovementType() == IEntityMovementType.MOVE_VTOL_RUN)
                    || (step.getMovementType() == IEntityMovementType.MOVE_SUBMARINE_WALK)
                    || (step.getMovementType() == IEntityMovementType.MOVE_SUBMARINE_RUN)
                    || (step.getElevation() != 0)) {
                costStringBuf.append("{").append(step.getElevation()).append(
                "}");
            }

            // Convert the buffer to a String and draw it.
            costString = costStringBuf.toString();
            graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
            int costX = stepPos.x + 42;
            if (shiftFlag) {
                costX -= (graph.getFontMetrics(graph.getFont()).stringWidth(
                        costString) / 2);
            }
            graph.setColor(Color.darkGray);
            graph.drawString(costString, costX, stepPos.y + 39);
            graph.setColor(col);
            graph.drawString(costString, costX - 1, stepPos.y + 38);
        }

    }

    /**
     * Sprite and info for a C3 network. Does not actually use the image buffer
     * as this can be horribly inefficient for long diagonal lines.
     */
    private class C3Sprite extends Sprite {
        private Polygon C3Poly;

        protected int entityId;
        protected int masterId;
        protected Entity entityE;
        protected Entity entityM;

        Color spriteColor;

        public C3Sprite(Entity e, Entity m) {
            entityE = e;
            entityM = m;
            entityId = e.getId();
            masterId = m.getId();
            spriteColor = PlayerColors.getColor(e.getOwner()
                    .getColorIndex());

            if ((e.getPosition() == null) || (m.getPosition() == null)) {
                C3Poly = new Polygon();
                C3Poly.addPoint(0, 0);
                C3Poly.addPoint(1, 0);
                C3Poly.addPoint(0, 1);
                bounds = new Rectangle(C3Poly.getBounds());
                bounds.setSize(bounds.getSize().width + 1,
                        bounds.getSize().height + 1);
                image = null;
                return;
            }

            makePoly();

            // set bounds
            bounds = new Rectangle(C3Poly.getBounds());
            bounds.setSize(bounds.getSize().width + 1,
                    bounds.getSize().height + 1);

            // move poly to upper right of image
            C3Poly.translate(-bounds.getLocation().x, -bounds.getLocation().y);

            // set names & stuff

            // nullify image
            image = null;
        }

        @Override
        public void prepare() {
        }

        private void makePoly() {
            // make a polygon
            final Point a = getHexLocation(entityE.getPosition());
            final Point t = getHexLocation(entityM.getPosition());

            final double an = (entityE.getPosition().radian(
                    entityM.getPosition()) + (Math.PI * 1.5))
                    % (Math.PI * 2); // angle
            final double lw = scale * C3_LINE_WIDTH; // line width

            C3Poly = new Polygon();
            C3Poly.addPoint(a.x
                    + (int) (scale * (HEX_W / 2) - (int) Math.round(Math
                            .sin(an)
                            * lw)), a.y
                            + (int) (scale * (HEX_H / 2) + (int) Math.round(Math
                                    .cos(an)
                                    * lw)));
            C3Poly.addPoint(a.x
                    + (int) (scale * (HEX_W / 2) + (int) Math.round(Math
                            .sin(an)
                            * lw)), a.y
                            + (int) (scale * (HEX_H / 2) - (int) Math.round(Math
                                    .cos(an)
                                    * lw)));
            C3Poly.addPoint(t.x
                    + (int) (scale * (HEX_W / 2) + (int) Math.round(Math
                            .sin(an)
                            * lw)), t.y
                            + (int) (scale * (HEX_H / 2) - (int) Math.round(Math
                                    .cos(an)
                                    * lw)));
            C3Poly.addPoint(t.x
                    + (int) (scale * (HEX_W / 2) - (int) Math.round(Math
                            .sin(an)
                            * lw)), t.y
                            + (int) (scale * (HEX_H / 2) + (int) Math.round(Math
                                    .cos(an)
                                    * lw)));
        }

        @Override
        public Rectangle getBounds() {
            makePoly();
            // set bounds
            bounds = new Rectangle(C3Poly.getBounds());
            bounds.setSize(bounds.getSize().width + 1,
                    bounds.getSize().height + 1);

            // move poly to upper right of image
            C3Poly.translate(-bounds.getLocation().x, -bounds.getLocation().y);
            image = null;

            return bounds;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            // makePoly();

            Polygon drawPoly = new Polygon(C3Poly.xpoints, C3Poly.ypoints,
                    C3Poly.npoints);
            drawPoly.translate(x, y);

            g.setColor(spriteColor);
            g.fillPolygon(drawPoly);
            g.setColor(Color.black);
            g.drawPolygon(drawPoly);
        }

        /**
         * Return true if the point is inside our polygon
         */
        @Override
        public boolean isInside(Point point) {
            return C3Poly.contains(point.x + view.x - bounds.x - offset.x,
                    point.y + view.y - bounds.y - offset.y);
        }

    }

    /**
     * Sprite and info for an attack. Does not actually use the image buffer as
     * this can be horribly inefficient for long diagonal lines. Appears as an
     * arrow. Arrow becoming cut in half when two Meks attacking each other.
     */
    private class AttackSprite extends Sprite {
        private ArrayList<AttackAction> attacks = new ArrayList<AttackAction>();
        private Point a;
        private Point t;
        private double an;
        private StraightArrowPolygon attackPoly;
        private Color attackColor;
        private int entityId;
        private int targetType;
        private int targetId;
        private String attackerDesc;
        private String targetDesc;
        ArrayList<String> weaponDescs = new ArrayList<String>();
        private final Entity ae;
        private final Targetable target;

        public AttackSprite(AttackAction attack) {
            attacks.add(attack);
            entityId = attack.getEntityId();
            targetType = attack.getTargetType();
            targetId = attack.getTargetId();
            ae = game.getEntity(attack.getEntityId());
            target = game.getTarget(targetType, targetId);

            // color?
            attackColor = PlayerColors.getColor(ae.getOwner().getColorIndex());
            // angle of line connecting two hexes
            an = (ae.getPosition().radian(target.getPosition()) + (Math.PI * 1.5))
            % (Math.PI * 2); // angle
            makePoly();

            // set bounds
            bounds = new Rectangle(attackPoly.getBounds());
            bounds.setSize(bounds.getSize().width + 1,
                    bounds.getSize().height + 1);
            // move poly to upper right of image
            attackPoly.translate(-bounds.getLocation().x,
                    -bounds.getLocation().y);

            // set names & stuff
            attackerDesc = ae.getDisplayName();
            targetDesc = target.getDisplayName();
            if (attack instanceof WeaponAttackAction) {
                addWeapon((WeaponAttackAction) attack);
            }
            if (attack instanceof KickAttackAction) {
                addWeapon((KickAttackAction) attack);
            }
            if (attack instanceof PunchAttackAction) {
                addWeapon((PunchAttackAction) attack);
            }
            if (attack instanceof PushAttackAction) {
                addWeapon((PushAttackAction) attack);
            }
            if (attack instanceof ClubAttackAction) {
                addWeapon((ClubAttackAction) attack);
            }
            if (attack instanceof ChargeAttackAction) {
                addWeapon((ChargeAttackAction) attack);
            }
            if (attack instanceof DfaAttackAction) {
                addWeapon((DfaAttackAction) attack);
            }
            if (attack instanceof ProtomechPhysicalAttackAction) {
                addWeapon((ProtomechPhysicalAttackAction) attack);
            }
            if (attack instanceof SearchlightAttackAction) {
                addWeapon((SearchlightAttackAction) attack);
            }

            // nullify image
            image = null;
        }

        private void makePoly() {
            // make a polygon
            a = getHexLocation(ae.getPosition());
            t = getHexLocation(target.getPosition());
            // OK, that is actually not good. I do not like hard coded figures.
            // HEX_W/2 - x distance in pixels from origin of hex bounding box to
            // the center of hex.
            // HEX_H/2 - y distance in pixels from origin of hex bounding box to
            // the center of hex.
            // 18 - is actually 36/2 - we do not want arrows to start and end
            // directly
            // in the centes of hex and hiding mek under.

            a.x = a.x + (int) (HEX_W / 2 * scale)
            + (int) Math.round(Math.cos(an) * (int) (18 * scale));
            t.x = t.x + (int) (HEX_W / 2 * scale)
            - (int) Math.round(Math.cos(an) * (int) (18 * scale));
            a.y = a.y + (int) (HEX_H / 2 * scale)
            + (int) Math.round(Math.sin(an) * (int) (18 * scale));
            t.y = t.y + (int) (HEX_H / 2 * scale)
            - (int) Math.round(Math.sin(an) * (int) (18 * scale));

            // Checking if given attack is mutual. In this case we building
            // halved arrow
            if (isMutualAttack()) {
                attackPoly = new StraightArrowPolygon(a, t, (int) (8 * scale),
                        (int) (12 * scale), true);
            } else {
                attackPoly = new StraightArrowPolygon(a, t, (int) (4 * scale),
                        (int) (8 * scale), false);
            }
        }

        @Override
        public Rectangle getBounds() {
            makePoly();
            // set bounds
            bounds = new Rectangle(attackPoly.getBounds());
            bounds.setSize(bounds.getSize().width + 1,
                    bounds.getSize().height + 1);
            // move poly to upper right of image
            attackPoly.translate(-bounds.getLocation().x,
                    -bounds.getLocation().y);

            return bounds;
        }

        /**
         * If we have build full arrow already with single attack and have got
         * counter attack from our target lately - lets change arrow to halved.
         */
        public void rebuildToHalvedPolygon() {
            attackPoly = new StraightArrowPolygon(a, t, (int) (8 * scale),
                    (int) (12 * scale), true);
            // set bounds
            bounds = new Rectangle(attackPoly.getBounds());
            bounds.setSize(bounds.getSize().width + 1,
                    bounds.getSize().height + 1);
            // move poly to upper right of image
            attackPoly.translate(-bounds.getLocation().x,
                    -bounds.getLocation().y);
        }

        /**
         * Cheking if attack is mutual and changing target arrow to half-arrow
         */
        private boolean isMutualAttack() {
            for (AttackSprite sprite : attackSprites) {
                if ((sprite.getEntityId() == targetId)
                        && (sprite.getTargetId() == entityId)) {
                    sprite.rebuildToHalvedPolygon();
                    return true;
                }
            }
            return false;
        }

        @Override
        public void prepare() {
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            Polygon drawPoly = new Polygon(attackPoly.xpoints,
                    attackPoly.ypoints, attackPoly.npoints);
            drawPoly.translate(x, y);

            g.setColor(attackColor);
            g.fillPolygon(drawPoly);
            g.setColor(Color.gray.darker());
            g.drawPolygon(drawPoly);
        }

        /**
         * Return true if the point is inside our polygon
         */
        @Override
        public boolean isInside(Point point) {
            return attackPoly.contains(point.x + view.x - bounds.x - offset.x,
                    point.y + view.y - bounds.y - offset.y);
        }

        public int getEntityId() {
            return entityId;
        }

        public int getTargetId() {
            return targetId;
        }

        /**
         * Adds a weapon to this attack
         */
        public void addWeapon(WeaponAttackAction attack) {
            final Entity entity = game.getEntity(attack.getEntityId());
            final WeaponType wtype = (WeaponType) entity.getEquipment(
                    attack.getWeaponId()).getType();
            final String roll = attack.toHit(game).getValueAsString();
            final String table = attack.toHit(game).getTableDesc();
            weaponDescs
            .add(wtype.getName()
                    + Messages.getString("BoardView1.needs") + roll + " " + table); //$NON-NLS-1$
        }

        public void addWeapon(KickAttackAction attack) {
            String bufer = ""; //$NON-NLS-1$
            String rollLeft = ""; //$NON-NLS-1$
            String rollRight = ""; //$NON-NLS-1$
            final int leg = attack.getLeg();
            switch (leg) {
            case KickAttackAction.BOTH:
                rollLeft = KickAttackAction.toHit(
                        game,
                        attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack
                                .getTargetId()), KickAttackAction.LEFT)
                                .getValueAsString();
                rollRight = KickAttackAction.toHit(
                        game,
                        attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack
                                .getTargetId()), KickAttackAction.RIGHT)
                                .getValueAsString();
                bufer = Messages
                .getString(
                        "BoardView1.kickBoth", new Object[] { rollLeft, rollRight }); //$NON-NLS-1$
                break;
            case KickAttackAction.LEFT:
                rollLeft = KickAttackAction.toHit(
                        game,
                        attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack
                                .getTargetId()), KickAttackAction.LEFT)
                                .getValueAsString();
                bufer = Messages.getString(
                        "BoardView1.kickLeft", new Object[] { rollLeft }); //$NON-NLS-1$
                break;
            case KickAttackAction.RIGHT:
                rollRight = KickAttackAction.toHit(
                        game,
                        attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack
                                .getTargetId()), KickAttackAction.RIGHT)
                                .getValueAsString();
                bufer = Messages.getString(
                        "BoardView1.kickRight", new Object[] { rollRight }); //$NON-NLS-1$
                break;
            }
            weaponDescs.add(bufer);
        }

        public void addWeapon(PunchAttackAction attack) {
            String bufer = ""; //$NON-NLS-1$
            String rollLeft = ""; //$NON-NLS-1$
            String rollRight = ""; //$NON-NLS-1$
            final int arm = attack.getArm();
            switch (arm) {
            case PunchAttackAction.BOTH:
                rollLeft = PunchAttackAction.toHit(
                        game,
                        attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack
                                .getTargetId()), PunchAttackAction.LEFT)
                                .getValueAsString();
                rollRight = PunchAttackAction.toHit(
                        game,
                        attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack
                                .getTargetId()), PunchAttackAction.RIGHT)
                                .getValueAsString();
                bufer = Messages
                .getString(
                        "BoardView1.punchBoth", new Object[] { rollLeft, rollRight }); //$NON-NLS-1$
                break;
            case PunchAttackAction.LEFT:
                rollLeft = PunchAttackAction.toHit(
                        game,
                        attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack
                                .getTargetId()), PunchAttackAction.LEFT)
                                .getValueAsString();
                bufer = Messages.getString(
                        "BoardView1.punchLeft", new Object[] { rollLeft }); //$NON-NLS-1$
                break;
            case PunchAttackAction.RIGHT:
                rollRight = PunchAttackAction.toHit(
                        game,
                        attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack
                                .getTargetId()), PunchAttackAction.RIGHT)
                                .getValueAsString();
                bufer = Messages
                .getString(
                        "BoardView1.punchRight", new Object[] { rollRight }); //$NON-NLS-1$
                break;
            }
            weaponDescs.add(bufer);
        }

        public void addWeapon(PushAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            weaponDescs.add(Messages.getString(
                    "BoardView1.push", new Object[] { roll })); //$NON-NLS-1$
        }

        public void addWeapon(ClubAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            final String club = attack.getClub().getName();
            weaponDescs.add(Messages.getString(
                    "BoardView1.hit", new Object[] { club, roll })); //$NON-NLS-1$
        }

        public void addWeapon(ChargeAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            weaponDescs.add(Messages.getString(
                    "BoardView1.charge", new Object[] { roll })); //$NON-NLS-1$
        }

        public void addWeapon(DfaAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            weaponDescs.add(Messages.getString(
                    "BoardView1.DFA", new Object[] { roll })); //$NON-NLS-1$
        }

        public void addWeapon(ProtomechPhysicalAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            weaponDescs.add(Messages.getString(
                    "BoardView1.proto", new Object[] { roll })); //$NON-NLS-1$
        }

        public void addWeapon(SearchlightAttackAction attack) {
            weaponDescs.add(Messages.getString("BoardView1.Searchlight"));
        }

        @Override
        public String[] getTooltip() {
            String[] tipStrings = new String[1 + weaponDescs.size()];
            int tip = 1;
            tipStrings[0] = attackerDesc
            + " " + Messages.getString("BoardView1.on") + " " + targetDesc; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            for (Iterator<String> i = weaponDescs.iterator(); i.hasNext();) {
                tipStrings[tip++] = i.next();
            }
            return tipStrings;
        }
    }

    /**
     * Sprite and info for movement vector (AT2 advanced movement).
     *  Does not actually use the image buffer
     * as this can be horribly inefficient for long diagonal lines.
     *
     * Appears as an arrow pointing to the hex this entity will move to
     * based on current movement vectors.
     * TODO: Different color depending upon whether entity has already moved
     * this turn
     *
     */
    private class MovementSprite extends Sprite
    {
        private Point a;
        private Point t;
        private double an;
        private StraightArrowPolygon movePoly;
        private Color moveColor;
        //private MovementVector mv;
        private int[] vectors;
        private Coords start;
        private Coords end;
        private Entity en;
        private int vel;

        public MovementSprite(Entity e, int[] v, Color col, boolean isCurrent) {
            //this.mv = en.getMV();

            en = e;
            vectors = v;//en.getVectors();
            //get the starting and ending position
            start = en.getPosition();
            end = Compute.getFinalPosition(start, vectors);

            //what is the velocity
            vel = 0;
            for (int element : v) {
                vel += element;
            }

            // color?
            //player colors
            moveColor = PlayerColors.getColor(en.getOwner().getColorIndex());
            //TODO: Its not going transparent. Oh well, it is a minor issue at the moment
            /*
            if(isCurrent) {
                int colour = col.getRGB();
                int transparency = GUIPreferences.getInstance().getInt(GUIPreferences.ADVANCED_ATTACK_ARROW_TRANSPARENCY);
                moveColor = new Color(colour | (transparency << 24), true);
            }
             */
            //red if offboard
            if(!game.getBoard().contains(end)) {
                int colour = 0xff0000; //red
                int transparency = GUIPreferences.getInstance().getInt(GUIPreferences.ADVANCED_ATTACK_ARROW_TRANSPARENCY);
                moveColor = new Color(colour | (transparency << 24), true);
            }
            //dark gray if done
            if(en.isDone()) {
                int colour = 0x696969; //gray
                int transparency = GUIPreferences.getInstance().getInt(GUIPreferences.ADVANCED_ATTACK_ARROW_TRANSPARENCY);
                moveColor = new Color(colour | (transparency << 24), true);
            }

            //moveColor = PlayerColors.getColor(en.getOwner().getColorIndex());
            //angle of line connecting two hexes
            an = (start.radian(end) + (Math.PI * 1.5)) % (Math.PI * 2); // angle
            makePoly();

            // set bounds
            bounds = new Rectangle(movePoly.getBounds());
            bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);
            // move poly to upper right of image
            movePoly.translate(-bounds.getLocation().x, -bounds.getLocation().y);

            // nullify image
            image = null;
        }

        private void makePoly(){
            // make a polygon
            a = getHexLocation(start);
            t = getHexLocation(end);
            // OK, that is actually not good. I do not like hard coded figures.
            // HEX_W/2 - x distance in pixels from origin of hex bounding box to the center of hex.
            // HEX_H/2 - y distance in pixels from origin of hex bounding box to the center of hex.
            // 18 - is actually 36/2 - we do not want arrows to start and end directly
            // in the centes of hex and hiding mek under.

            a.x = a.x + (int)(HEX_W/2*scale) + (int)Math.round(Math.cos(an) * (int)(18*scale));
            t.x = t.x + (int)(HEX_W/2*scale) - (int)Math.round(Math.cos(an) * (int)(18*scale));
            a.y = a.y + (int)(HEX_H/2*scale) + (int)Math.round(Math.sin(an) * (int)(18*scale));
            t.y = t.y + (int)(HEX_H/2*scale) - (int)Math.round(Math.sin(an) * (int)(18*scale));
            movePoly = new StraightArrowPolygon(a, t, (int)(4*scale), (int)(8*scale), false);
        }

        @Override
        public Rectangle getBounds(){
            makePoly();
            // set bounds
            bounds = new Rectangle(movePoly.getBounds());
            bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);
            // move poly to upper right of image
            movePoly.translate(-bounds.getLocation().x, -bounds.getLocation().y);

            return bounds;
        }

        @Override
        public void prepare() {

        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            //don't draw anything if the unit has no velocity

            if(vel == 0) {
                return;
            }

            Polygon drawPoly = new Polygon(movePoly.xpoints, movePoly.ypoints, movePoly.npoints);
            drawPoly.translate(x, y);

            g.setColor(moveColor);
            g.fillPolygon(drawPoly);
            g.setColor(Color.gray.darker());
            g.drawPolygon(drawPoly);


        }

        /**
         * Return true if the point is inside our polygon
         */
        @Override
        public boolean isInside(Point point) {
            return movePoly.contains(point.x + view.x - bounds.x - offset.x,
                    point.y + view.y - bounds.y - offset.y);
        }

        /*
        public String[] getTooltip() {
            String[] tipStrings = new String[1 + weaponDescs.size()];
            int tip = 1;
            tipStrings[0] = attackerDesc + " "+Messages.getString("BoardView1.on")+" " + targetDesc; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            for (Iterator<String> i = weaponDescs.iterator(); i.hasNext();) {
                tipStrings[tip++] = i.next();
            }
            return tipStrings;
        }
         */
    }

    /**
     * Determine if the tile manager's images have been loaded.
     *
     * @return <code>true</code> if all images have been loaded.
     *         <code>false</code> if more need to be loaded.
     */
    public boolean isTileImagesLoaded() {
        return tileManager.isLoaded();
    }

    public void setUseLOSTool(boolean use) {
        useLOSTool = use;
    }

    public TilesetManager getTilesetManager() {
        return tileManager;
    }

    public void drawRuler(Coords s, Coords e, Color sc, Color ec) {
        rulerStart = s;
        rulerEnd = e;
        rulerStartColor = sc;
        rulerEndColor = ec;

        repaint();
    }

    /**
     * @param lastCursor The lastCursor to set.
     */
    public void setLastCursor(Coords lastCursor) {
        this.lastCursor = lastCursor;
    }

    /**
     * @return Returns the lastCursor.
     */
    public Coords getLastCursor() {
        return lastCursor;
    }

    /**
     * @param highlighted The highlighted to set.
     */
    public void setHighlighted(Coords highlighted) {
        this.highlighted = highlighted;
    }

    /**
     * @return Returns the highlighted.
     */
    public Coords getHighlighted() {
        return highlighted;
    }

    /**
     * @param selected The selected to set.
     */
    public void setSelected(Coords selected) {
        this.selected = selected;
    }

    /**
     * @return Returns the selected.
     */
    public Coords getSelected() {
        return selected;
    }

    /**
     * @param firstLOS The firstLOS to set.
     */
    public void setFirstLOS(Coords firstLOS) {
        this.firstLOS = firstLOS;
    }

    /**
     * @return Returns the firstLOS.
     */
    public Coords getFirstLOS() {
        return firstLOS;
    }

    /**
     * Determines if this Board contains the Coords, and if so, "selects" that
     * Coords.
     *
     * @param coords the Coords.
     */
    public void select(Coords coords) {
        if ((coords == null) || game.getBoard().contains(coords)) {
            setSelected(coords);
            moveCursor(selectedSprite, coords);
            moveCursor(firstLOSSprite, null);
            moveCursor(secondLOSSprite, null);
            processBoardViewEvent(new BoardViewEvent(this, coords, null,
                    BoardViewEvent.BOARD_HEX_SELECTED, 0));
        }
    }

    /**
     * "Selects" the specified Coords.
     *
     * @param x the x coordinate.
     * @param y the y coordinate.
     */
    public void select(int x, int y) {
        select(new Coords(x, y));
    }

    /**
     * Determines if this Board contains the Coords, and if so, highlights that
     * Coords.
     *
     * @param coords the Coords.
     */
    public void highlight(Coords coords) {
        if ((coords == null) || game.getBoard().contains(coords)) {
            setHighlighted(coords);
            moveCursor(highlightSprite, coords);
            moveCursor(firstLOSSprite, null);
            moveCursor(secondLOSSprite, null);
            processBoardViewEvent(new BoardViewEvent(this, coords, null,
                    BoardViewEvent.BOARD_HEX_HIGHLIGHTED, 0));
        }
    }

    /**
     * Highlights the specified Coords.
     *
     * @param x the x coordinate.
     * @param y the y coordinate.
     */
    public void highlight(int x, int y) {
        highlight(new Coords(x, y));
    }

    /**
     * Determines if this Board contains the Coords, and if so, "cursors" that
     * Coords.
     *
     * @param coords the Coords.
     */
    public void cursor(Coords coords) {
        if ((coords == null) || game.getBoard().contains(coords)) {
            if ((getLastCursor() == null) || (coords == null)
                    || !coords.equals(getLastCursor())) {
                setLastCursor(coords);
                moveCursor(cursorSprite, coords);
                moveCursor(firstLOSSprite, null);
                moveCursor(secondLOSSprite, null);
                processBoardViewEvent(new BoardViewEvent(this, coords, null,
                        BoardViewEvent.BOARD_HEX_CURSOR, 0));
            } else {
                setLastCursor(coords);
            }
        }
    }

    /**
     * "Cursors" the specified Coords.
     *
     * @param x the x coordinate.
     * @param y the y coordinate.
     */
    public void cursor(int x, int y) {
        cursor(new Coords(x, y));
    }

    public void checkLOS(Coords c) {
        if ((c == null) || game.getBoard().contains(c)) {
            if (getFirstLOS() == null) {
                setFirstLOS(c);
                firstLOSHex(c);
                processBoardViewEvent(new BoardViewEvent(this, c, null,
                        BoardViewEvent.BOARD_FIRST_LOS_HEX, 0));
            } else {
                secondLOSHex(c, getFirstLOS());
                processBoardViewEvent(new BoardViewEvent(this, c, null,
                        BoardViewEvent.BOARD_SECOND_LOS_HEX, 0));
                setFirstLOS(null);
            }
        }
    }

    /**
     * Determines if this Board contains the (x, y) Coords, and if so, notifies
     * listeners about the specified mouse action.
     */
    public void mouseAction(int x, int y, int mtype, int modifiers) {
        if (game.getBoard().contains(x, y)) {
            Coords c = new Coords(x, y);
            switch (mtype) {
            case BOARD_HEX_CLICK:
                if ((modifiers & java.awt.event.InputEvent.CTRL_MASK) != 0) {
                    checkLOS(c);
                } else {
                    processBoardViewEvent(new BoardViewEvent(this, c, null,
                            BoardViewEvent.BOARD_HEX_CLICKED, modifiers));
                }
                break;
            case BOARD_HEX_DOUBLECLICK:
                processBoardViewEvent(new BoardViewEvent(this, c, null,
                        BoardViewEvent.BOARD_HEX_DOUBLECLICKED, modifiers));
                break;
            case BOARD_HEX_DRAG:
                processBoardViewEvent(new BoardViewEvent(this, c, null,
                        BoardViewEvent.BOARD_HEX_DRAGGED, modifiers));
                break;
            case BOARD_HEX_POPUP :
                processBoardViewEvent(new BoardViewEvent(this, c, null, BoardViewEvent.BOARD_HEX_POPUP, modifiers));
                break;
            }
        }
    }

    /**
     * Notifies listeners about the specified mouse action.
     *
     * @param coords the Coords.
     */
    public void mouseAction(Coords coords, int mtype, int modifiers) {
        mouseAction(coords.x, coords.y, mtype, modifiers);
    }

    /**
     * Return, whether a popup may be drawn, this currently means, whether no
     * scrolling took place.
     */
    public boolean mayDrawPopup() {
        return !scrolled;
    }

    /*
     * (non-Javadoc)
     *
     * @see megamek.common.BoardListener#boardNewBoard(megamek.common.BoardEvent)
     */
    public void boardNewBoard(BoardEvent b) {
        updateBoard();
    }

    /*
     * (non-Javadoc)
     *
     * @see megamek.common.BoardListener#boardChangedHex(megamek.common.BoardEvent)
     */
    public synchronized void boardChangedHex(BoardEvent b) {
        IHex hex = game.getBoard().getHex(b.getCoords());
        tileManager.clearHex(hex);
        tileManager.waitForHex(hex);
        if (boardGraph != null) {
            redrawAround(b.getCoords());
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see megamek.common.BoardListener#boardChangedHex(megamek.common.BoardEvent)
     */
    public synchronized void boardChangedAllHexes(BoardEvent b) {
        updateBoardImage();
    }

    private GameListener gameListener = new GameListenerAdapter() {

        @Override
        public void gameEntityNew(GameEntityNewEvent e) {
            updateEcmList();
            redrawAllEntities();
            if(game.getPhase() == IGame.Phase.PHASE_MOVEMENT) {
                refreshMoveVectors();
            }
        }

        @Override
        public void gameEntityRemove(GameEntityRemoveEvent e) {
            updateEcmList();
            redrawAllEntities();
            if(game.getPhase() == IGame.Phase.PHASE_MOVEMENT) {
                refreshMoveVectors();
            }
        }

        @Override
        public void gameEntityChange(GameEntityChangeEvent e) {
            Vector<UnitLocation> mp = e.getMovePath();
            updateEcmList();
            if (e.getEntity().hasActiveECM()) {
                redrawAllEntities();
            }
            if(game.getPhase() == IGame.Phase.PHASE_MOVEMENT) {
                refreshMoveVectors();
            }
            if ((mp != null) && (mp.size() > 0)
                    && GUIPreferences.getInstance().getShowMoveStep()) {
                addMovingUnit(e.getEntity(), mp);
            } else {
                redrawEntity(e.getEntity());
            }
        }

        @Override
        public void gameNewAction(GameNewActionEvent e) {
            EntityAction ea = e.getAction();
            if (ea instanceof AttackAction) {
                addAttack((AttackAction) ea);
            }
        }

        @Override
        public void gameBoardNew(GameBoardNewEvent e) {
            IBoard b = e.getOldBoard();
            if (b != null) {
                b.removeBoardListener(BoardView1.this);
            }
            b = e.getNewBoard();
            if (b != null) {
                b.addBoardListener(BoardView1.this);
            }
            updateBoard();
        }

        @Override
        public void gameBoardChanged(GameBoardChangeEvent e) {
            boardChanged();
        }

        @Override
        public void gamePhaseChange(GamePhaseChangeEvent e) {
            refreshAttacks();
            switch (e.getNewPhase()) {
            case PHASE_MOVEMENT:
                refreshMoveVectors();
            case PHASE_FIRING:
                clearAllMoveVectors();
            case PHASE_PHYSICAL:
                refreshAttacks();
                break;
            case PHASE_INITIATIVE:
                clearAllAttacks();
                break;
            case PHASE_END:
            case PHASE_VICTORY:
                clearSprites();
            }
        }
    };

    synchronized void boardChanged() {
        redrawAllEntities();
    }

    void clearSprites() {
        pathSprites.clear();
        attackSprites.clear();
        C3Sprites.clear();
        movementSprites.clear();

    }

    protected synchronized void updateBoard() {
        updateBoardSize();
        if (backGraph != null) {
            backGraph.dispose();
        }
        backGraph = null;
        backImage = null;
        backSize = null;
        boardImage = null;
        if (boardGraph != null) {
            boardGraph.dispose();
        }
        boardGraph = null;
        redrawAllEntities();
    }

    /**
     * the old redrawworker converted to a runnable which is called now and then
     * from the event thread
     */
    protected class RedrawWorker implements Runnable {

        protected long lastTime = System.currentTimeMillis();
        protected long currentTime = System.currentTimeMillis();

        /**
         * somebody please document wth this is doing..- itmo
         */
        public void run() {
            currentTime = System.currentTimeMillis();
            if (isShowing()) {
                boolean redraw = false;
                for (int i = 0; i < displayables.size(); i++) {
                    IDisplayable disp = displayables.get(i);
                    if (!disp.isSliding()) {
                        disp.setIdleTime(currentTime - lastTime, true);
                    } else {
                        redraw = redraw || disp.slide();
                    }
                }
                if (backSize != null) {
                    redraw = redraw || doMoveUnits(currentTime - lastTime);
                    redraw = redraw || doScroll();
                    checkTooltip();
                } else {
                    repaint(100);
                }
                if (redraw) {
                    repaint();
                }
            }
            lastTime = currentTime;
        }
    }

    public synchronized void WeaponSelected(MechDisplayEvent b) {
        selectedEntity = b.getEntity();
        selectedWeapon = b.getEquip();
        repaint(100);
    }

    private class EcmBubble extends Coords {
        private static final long serialVersionUID = -1605350790342525964L;
        int range;
        int tint;
        int direction;

        public EcmBubble(Coords c, int range, int tint) {
            super(c);
            this.range = range;
            this.tint = tint;
            direction = -1;
        }

        public EcmBubble(Coords c, int range, int tint, int direction) {
            super(c);
            this.range = range;
            this.tint = tint;
            this.direction = direction;
        }
    }

    // This is expensive, so precalculate when entity changes
    public void updateEcmList() {
        ArrayList<EcmBubble> list = new ArrayList<EcmBubble>();
        for (Enumeration<Entity> e = game.getEntities(); e.hasMoreElements();) {
            Entity ent = e.nextElement();
            Coords entPos = ent.getPosition();
            int range = ent.getECMRange();
            boolean deployed = ent.isDeployed();
            boolean offboard = ent.isOffBoard();
            if((entPos == null) && (ent.getTransportId() != Entity.NONE)) {
                Entity carrier = game.getEntity(ent.getTransportId());
                if((null != carrier) && carrier.loadedUnitsHaveActiveECM()) {
                    entPos = carrier.getPosition();
                    deployed = carrier.isDeployed();
                    offboard = carrier.isOffBoard();
                }
            }
            if ((entPos == null) || !deployed
                    || offboard) {
                continue;
            }
            if (range != Entity.NONE) {
                int tint = PlayerColors.getColorRGB(ent.getOwner()
                        .getColorIndex());
                list.add(new EcmBubble(entPos, range, tint));
            }
            if(game.getBoard().inSpace()) {
                //then BAP is also ECCM so it needs a bubble
                range = ent.getBAPRange();
                int direction = -1;
                if (range != Entity.NONE) {
                    if(range > 6) {
                        direction = ent.getFacing();
                    }
                    int tint = PlayerColors.getColorRGB(ent.getOwner()
                            .getColorIndex());
                    list.add(new EcmBubble(entPos, range, tint, direction));
                }
            }
        }
        HashMap<Coords, Integer> table = new HashMap<Coords, Integer>();
        for (EcmBubble b : list) {
            Integer col = new Integer(b.tint);
            for (int x = -b.range; x <= b.range; x++) {
                for (int y = -b.range; y <= b.range; y++) {
                    Coords c = new Coords(x + b.x, y + b.y);
                    // clip rectangle to hexagon
                    if ((b.distance(c) <= b.range) && ((b.direction == -1) || Compute.isInArc(b, b.direction, c, Compute.ARC_NOSE))) {
                        Integer tint = table.get(c);
                        if (tint == null) {
                            table.put(c, col);
                        } else if (tint.intValue() != b.tint) {
                            int red1 = (tint.intValue() >> 16) & 0xff;
                            int green1 = (tint.intValue() >> 8) & 0xff;
                            int blue1 = tint.intValue() & 0xff;
                            int red2 = (b.tint >> 16) & 0xff;
                            int green2 = (b.tint >> 8) & 0xff;
                            int blue2 = b.tint & 0xff;
                            red1 = (red1 + red2) / 2;
                            green1 = (green1 + green2) / 2;
                            blue1 = (blue1 + blue2) / 2;
                            table.put(c, new Integer((red1 << 16)
                                    | (green1 << 8) | blue1));
                        }
                    }
                }
            }
        }
        synchronized (this) {
            ecmHexes = table;
            dirtyBoard = true;
        }
        repaint(100);
    }

    /**
     * Have the player select an Entity from the entities at the given coords.
     *
     * @param pos - the <code>Coords</code> containing targets.
     */
    private Entity chooseEntity(Coords pos) {

        // Assume that we have *no* choice.
        Entity choice = null;

        // Get the available choices.
        Enumeration<Entity> choices = game.getEntities(pos);

        // Convert the choices into a List of targets.
        Vector<Entity> entities = new Vector<Entity>();
        while (choices.hasMoreElements()) {
            entities.addElement(choices.nextElement());
        }

        // Do we have a single choice?
        if (entities.size() == 1) {
            // Return that choice.
            choice = entities.elementAt(0);
        }

        // If we have multiple choices, display a selection dialog.
        else if (entities.size() > 1) {
            String[] names = new String[entities.size()];
            for (int loop = 0; loop < names.length; loop++) {
                names[loop] = entities.elementAt(loop).getDisplayName();
            }
            SingleChoiceDialog choiceDialog = new SingleChoiceDialog(
                    (Frame)SwingUtilities.getAncestorOfClass(Frame.class, this),
                    Messages.getString("BoardView1.ChooseEntityDialog.title"), //$NON-NLS-1$
                    Messages
                    .getString(
                            "BoardView1.ChooseEntityDialog.message", new Object[] { pos.getBoardNum() }), //$NON-NLS-1$
                            names);
            choiceDialog.setVisible(true);
            if (choiceDialog.getAnswer() == true) {
                choice = entities.elementAt(choiceDialog.getChoice());
            }
        } // End have-choices

        // Return the chosen unit.
        return choice;
    }

    public Component getComponent() {
        // Scrollbars are broken for "Brandon Drew" <brandx0@hotmail.com>
        if (System.getProperty
                ("megamek.client.clientgui.hidescrollbars", "false").equals //$NON-NLS-1$ //$NON-NLS-2$
                ("true")) {
            return this;
        }
        if (scroller != null) {
            return scroller;
        }

        // Place the board viewer in a set of scrollbars.
        scroller = new Panel();
        scroller.setLayout(new BorderLayout());
        vScrollbar = new Scrollbar(Scrollbar.VERTICAL);
        hScrollbar = new Scrollbar(Scrollbar.HORIZONTAL);
        scroller.add(this, BorderLayout.CENTER);

        // Assign the scrollbars to the board viewer.
        scroller.add(vScrollbar, BorderLayout.EAST);
        scroller.add(hScrollbar, BorderLayout.SOUTH);

        // When the scroll bars are adjusted, update our offset.
        vScrollbar.addAdjustmentListener (this);
        hScrollbar.addAdjustmentListener (this);

        return scroller;
    }

    public void refreshDisplayables() {
        repaint();
    }

    public void showPopup(Object popup, Coords c) {
        Point p = getHexLocation(c);
        p.x += (int)(HEX_WC*scale)-view.x;
        p.y += (int)(HEX_H*scale/2)-view.y;
        if (((PopupMenu)popup).getParent() == null) {
            add((PopupMenu)popup);
        }
        ((PopupMenu)popup).show(this, p.x, p.y);
    }

    public void refreshMinefields() {
        repaint();
    }
}
