/*
 * 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.swing;

import megamek.client.event.BoardViewEvent;
import megamek.client.event.BoardViewListener;
import megamek.client.event.MechDisplayEvent;
import megamek.client.event.MechDisplayListener;
import megamek.client.ui.AWT.Messages;
import megamek.client.ui.swing.util.ImageCache;
import megamek.client.ui.swing.util.ImprovedAveragingScaleFilter;
import megamek.client.ui.swing.util.KeyAlphaFilter;
import megamek.client.ui.swing.util.PlayerColors;
import megamek.client.ui.swing.util.StraightArrowPolygon;
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.Player;
import megamek.common.Protomech;
import megamek.common.Tank;
import megamek.common.TargetRoll;
import megamek.common.Targetable;
import megamek.common.Terrains;
import megamek.common.UnitLocation;
import megamek.common.WeaponResult;
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;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollBar;
import javax.swing.JWindow;
import javax.swing.ToolTipManager;
import java.awt.Adjustable;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Toolkit;
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.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;

/**
 * Displays the board; lets the user scroll around and select points on it.
 */
public final class BoardView1
        extends JComponent
        implements IBoardView, BoardListener, MouseListener, MouseMotionListener, KeyListener, AdjustmentListener, MechDisplayListener, IPreferenceChangeListener {
    private static final int TRANSPARENT = 0xFFFF00FF;
    // 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 final Font FONT_7 = new Font("SansSerif", Font.PLAIN, 7); //$NON-NLS-1$
    private static final Font FONT_8 = new Font("SansSerif", Font.PLAIN, 8); //$NON-NLS-1$
    private static final Font FONT_9 = new Font("SansSerif", Font.PLAIN, 9); //$NON-NLS-1$
    private static final Font FONT_10 = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
    private static final Font FONT_12 = new Font("SansSerif", Font.PLAIN, 12); //$NON-NLS-1$
    private Dimension hex_size = null;
    private Font font_hexnum = FONT_10;
    private Font font_elev = FONT_9;
    private Font font_minefield = FONT_12;
    private final IGame game;
    private final JFrame frame;
    private Point mousePos = new Point();
    private final Rectangle view = new Rectangle();
    private final Point offset = new Point();
    private Dimension boardSize;
    // scrolly stuff:
    private JScrollBar vScrollbar = null;
    private JScrollBar hScrollbar = null;
    private boolean isScrolling = false;
    private final 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;
    private 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<Sprite> entitySprites = new ArrayList<Sprite>();
    private HashMap<Integer, Sprite> entitySpriteIds = new HashMap<Integer, Sprite>();
    // sprites for the three selection cursors
    private final CursorSprite cursorSprite;
    private final CursorSprite highlightSprite;
    private final CursorSprite selectedSprite;
    private final CursorSprite firstLOSSprite;
    private final CursorSprite secondLOSSprite;
    // sprite for current movement
    private ArrayList<Sprite> pathSprites = new ArrayList<Sprite>();
    // vector of sprites for all firing lines
    private ArrayList<Sprite> attackSprites = new ArrayList<Sprite>();
    // vector of sprites for C3 network lines
    private ArrayList<Sprite> C3Sprites = new ArrayList<Sprite>();
    // tooltip stuff
    private JWindow tipWindow;
    private boolean isTipPossible = false;
    private long lastIdle;
    private TilesetManager tileManager = null;
    // polygons for a few things
    private Polygon hexPoly;
    private Polygon[] facingPolys;
    private 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;
    private int zoomIndex;
    private float scale;
    private ImageCache<Image,Image> scaledImageCache = new ImageCache();
        
    // Displayables (Chat box, etc.)
    private final Vector displayables = new Vector();
    // Move units step by step
    private final Vector movingUnits = new Vector();
    private long moveWait = 0;
    // moving entity sprites
    private ArrayList<Sprite> movingEntitySprites = new ArrayList<Sprite>();
    private HashMap<Integer, Sprite> movingEntitySpriteIds = new HashMap<Integer, Sprite>();
    private final ArrayList<Sprite> ghostEntitySprites = new ArrayList<Sprite>();
    private final transient Vector boardListeners = new Vector();
    // wreck sprites
    private ArrayList<Sprite> wreckSprites = new ArrayList<Sprite>();
    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;
    private final ClientGUI clientgui; //TODO please eliminate the usage of the clientgui
    private final RedrawWorker redrawWorker = new RedrawWorker();
    //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, JFrame frame) throws IOException {
        this(game, frame, null);
    }

    /**
     * Construct a new board view for the specified game
     */
    public BoardView1(IGame game, JFrame frame, ClientGUI clientgui) throws IOException {
        //TODO please eliminate the usage of the clientgui
        this.clientgui = clientgui;
        this.game = game;
        this.frame = frame;
        tileManager = new TilesetManager(this);
        game.addGameListener(gameListener);
        game.getBoard().addBoardListener(this);
        redrawWorker.start();
        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 JWindow(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);
        setToolTipText("");
        System.out.println("ToolTip delay is set at: "+ToolTipManager.sharedInstance().getInitialDelay());

        PreferenceManager.getClientPreferences().addPreferenceChangeListener(this);
    }

    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.addElement(listener);
        }
    }

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

    /**
     * Notifies attached board listeners of the event.
     *
     * @param event the board event.
     */
    public void processBoardViewEvent(BoardViewEvent event) {
        if (boardListeners == null) {
            return;
        }
        for (Enumeration e = boardListeners.elements(); e.hasMoreElements();) {
            BoardViewListener l = (BoardViewListener) e.nextElement();
            switch (event.getType()) {
                case BoardViewEvent.BOARD_HEX_CLICKED:
                case BoardViewEvent.BOARD_HEX_DOUBLECLICKED:
                case BoardViewEvent.BOARD_HEX_DRAGGED:
                    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;
            }
        }
    }

    private void addMovingUnit(Entity entity, Vector movePath) {
        if (!movePath.isEmpty()) {
            Object[] o = new Object[2];
            o[0] = entity;
            o[1] = movePath;
            movingUnits.addElement(o);
            GhostEntitySprite ghostSprite = new GhostEntitySprite(entity);
            ghostEntitySprites.add(ghostSprite);

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

    public void addDisplayable(Displayable disp) {
        displayables.addElement(disp);
    }

    /**
     * Specify the scrollbars that control this view's positioning.
     *
     * @param vertical   - the vertical <code>Scrollbar</code>
     * @param horizontal - the horizontal <code>Scrollbar</code>
     */
    public void setScrollbars(JScrollBar vertical, JScrollBar horizontal) {
        vScrollbar = vertical;
        hScrollbar = horizontal;

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

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

    public void paintComponent(Graphics g) {
        super.paintComponent(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 (vScrollbar != null) {
            vScrollbar.setVisibleAmount(size.height);
            vScrollbar.setBlockIncrement(size.height);
            vScrollbar.setUnitIncrement((int) (scale * HEX_H / 2.0));
            vScrollbar.setMaximum(boardSize.height);
        }
        if (hScrollbar != null) {
            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)) {
            updateBoardImage();
        }

        // draw onto the back buffer:

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

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

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

        // Artillery targets
        drawArtilleryHexes();

        // 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, if valid
        drawSprites(pathSprites);

        // added by kenn
        // 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);
        }
        // end kenn

        // draw all the "displayables"
        for (int i = 0; i < displayables.size(); i++) {
            Displayable disp = (Displayable) displayables.elementAt(i);
            disp.draw(backGraph, 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);
        }
        //g.drawString(""+scale, 10, 10);

        //         final long finishTime = System.currentTimeMillis();//commentme
        //         System.out.println("BoardView1: updated screen in " + (finishTime - startTime) + " ms."); //commentme
    }

    /**
     * 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<Sprite> sprites) {
        for (Sprite sprite : sprites) {
            drawSprite(sprite);
        }
    }

    /**
     * Draws a sprite, if it is in the current view
     */
    private 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.
     */
    private 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);

            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 static 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;
        }

        //TODO please eliminate the usage of the clientgui
        if (clientgui != null &&
                !selectedEntity.getOwner().equals(clientgui.getClient().getLocalPlayer())) {
            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;
    }

    /**
     * Display artillery modifier in pretargeted hexes
     */
    private void drawArtilleryHexes() {
        Mounted weapon = getSelectedArtilleryWeapon();
        if (game.getArtillerySize() == 0 && weapon == null) {
            return; //nothing to do
        }
        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();
        Image scaledImage;

        // 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 (weapon != null) {
                    //process targetted hexes
                    int amod;
                    //Check the predesignated hexes
                    if (selectedEntity.getOwner().getArtyAutoHitHexes().contains(c)) {
                        amod = TargetRoll.AUTOMATIC_SUCCESS;
                    } else {
                        amod = selectedEntity.aTracker.getModifier(weapon, c);
                    }
                    if (amod != 0) {
                        //draw the crosshairs
                        if (amod == TargetRoll.AUTOMATIC_SUCCESS) {
                            //predesignated or already hit
                            scaledImage = getScaledImage(tileManager.getArtilleryTarget(TilesetManager.ARTILLERY_AUTOHIT));
                        } else {
                            scaledImage = getScaledImage(tileManager.getArtilleryTarget(TilesetManager.ARTILLERY_ADJUSTED));
                        }
                        backGraph.drawImage(scaledImage, p.x, p.y, this);
                    }
                }
                //process incoming attacks - requires server to update client's view of game
                
                for (Enumeration attacks = game.getArtilleryAttacks(); attacks.hasMoreElements();) {
                    ArtilleryAttackAction a = (ArtilleryAttackAction) attacks.nextElement();
                    if (a.getWR().waa.getTarget(game).getPosition().equals(c)) {
                        scaledImage = getScaledImage(tileManager.getArtilleryTarget(TilesetManager.ARTILLERY_INCOMING));
                        backGraph.drawImage(scaledImage, p.x, p.y, this);
                        break; //do not draw multiple times, tooltop will show all attacks
                    }
                }
            }
        }
    }

    private Vector getArtilleryAttacksAtLocation(Coords c) {
        Vector v = new Vector();
        for (Enumeration attacks = game.getArtilleryAttacks(); attacks.hasMoreElements();) {
            ArtilleryAttackAction a = (ArtilleryAttackAction) attacks.nextElement();
            if (a.getWR().waa.getTarget(game).getPosition().equals(c)) {
                v.addElement(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 = (Minefield) 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"), //$NON-NLS-1$
                                    p.x,
                                    p.y + (int) (51 * scale),
                                    font_minefield,
                                    backGraph);
                            break;
                        case (Minefield.TYPE_THUNDER):
                            drawCenteredString(Messages.getString("BoardView1.Thunder") + mf.getDamage() + ")", //$NON-NLS-1$ //$NON-NLS-2$
                                    p.x,
                                    p.y + (int) (51 * scale),
                                    font_minefield,
                                    backGraph);
                            break;
                        case (Minefield.TYPE_THUNDER_INFERNO):
                            drawCenteredString(Messages.getString("BoardView1.Thunder-Inf") + mf.getDamage() + ")", //$NON-NLS-1$ //$NON-NLS-2$
                                    p.x,
                                    p.y + (int) (51 * scale),
                                    font_minefield,
                                    backGraph);
                            break;
                        case (Minefield.TYPE_THUNDER_ACTIVE):
                            drawCenteredString(Messages.getString("BoardView1.Thunder-Actv") + mf.getDamage() + ")", //$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-"), //$NON-NLS-1$
                                    p.x,
                                    p.y + (int) (51 * scale),
                                    font_minefield,
                                    backGraph);
                            drawCenteredString(Messages.getString("BoardView1.detonated"), //$NON-NLS-1$
                                    p.x,
                                    p.y + (int) (60 * scale),
                                    font_minefield,
                                    backGraph);
                            break;
                        case (Minefield.TYPE_VIBRABOMB):
                            drawCenteredString(Messages.getString("BoardView1.Vibrabomb"), //$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) {
        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));

        // 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 (Iterator i = tileManager.supersFor(hex).iterator(); i.hasNext();) {
                scaledImage = getScaledImage((Image) i.next());
                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.getOptions().booleanOption("night_battle") &&
                !game.isPositionIlluminated(c)) {
            scaledImage = getScaledImage(tileManager.getNightFog());
            boardGraph.drawImage(scaledImage, drawX, drawY, this);
        }
        boardGraph.setColor(GUIPreferences.getInstance().getMapTextColor());
        
        // 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 (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 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));
    }

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

    // added by kenn
    /**
     * 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);
    }
    // end kenn

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

    /**
     * The text to be displayed when the mouse is at a certain point
     */
    public String getToolTipText(MouseEvent me) {
        Point point = me.getPoint();
        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 (Iterator i = entitySprites.iterator(); i.hasNext();) {
            final EntitySprite eSprite = (EntitySprite) i.next();
            if (eSprite.isInside(point)) {
                stringsSize += 3;
            }
        }

        // check if it's on any attacks
        for (Iterator i = attackSprites.iterator(); i.hasNext();) {
            final AttackSprite aSprite = (AttackSprite) i.next();
            if (aSprite.isInside(point)) {
                stringsSize += 1 + aSprite.weaponDescs.size();
            }
        }

        // If the hex contains a building or rubble, make more space.
        // Also if it contains woods or jungle.
        if (mhex != null) {
            if (mhex.containsTerrain(Terrains.RUBBLE))
                stringsSize++;
            if (mhex.containsTerrain(Terrains.FUEL_TANK))
                stringsSize++;
            if (mhex.containsTerrain(Terrains.BUILDING))
                stringsSize++;
            if (mhex.containsTerrain(Terrains.BRIDGE))
                stringsSize++;
            if (mhex.containsTerrain(Terrains.JUNGLE)) 
                stringsSize ++;
            else if (mhex.containsTerrain(Terrains.WOODS)) 
                stringsSize ++;
        }
        stringsSize += game.getNbrMinefields(mcoords);
        
        // Artillery
        final Vector artilleryAttacks = getArtilleryAttacksAtLocation(mcoords);
        stringsSize += artilleryAttacks.size();

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

        // 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;

            if (mhex.containsTerrain(Terrains.JUNGLE)) {
                int ttl = mhex.getTerrain(Terrains.JUNGLE).getLevel();
                if (ttl == 1)
                    strings[stringsIndex] = "Light Jungle";
                else if (ttl == 2)
                    strings[stringsIndex] = "Heavy Jungle";
                else if (ttl == 3)
                    strings[stringsIndex] = "Ultra-Heavy Jungle";
                else
                    strings[stringsIndex] = "Jungle";
                stringsIndex += 1;
            } else if (mhex.containsTerrain(Terrains.WOODS)) {
                int ttl = mhex.getTerrain(Terrains.WOODS).getLevel();
                if (ttl == 1)
                    strings[stringsIndex] = "Light Woods";
                else if (ttl == 2)
                    strings[stringsIndex] = "Heavy Woods";
                else if (ttl == 3)
                    strings[stringsIndex] = "Ultra-Heavy Woods";
                else
                    strings[stringsIndex] = "Woods";
                stringsIndex += 1;
            }

            // Do we have rubble?
            if (mhex.containsTerrain(Terrains.RUBBLE)) {
                strings[stringsIndex] = Messages.getString("BoardView1.Rubble"); //$NON-NLS-1$
                stringsIndex += 1;
            }

            // Do we have a building?
            if ((mhex.containsTerrain(Terrains.FUEL_TANK)) || (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());
                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());
                strings[stringsIndex] = buf.toString();
                stringsIndex += 1;
            }
            if (game.containsMinefield(mcoords)) {
                Vector minefields = game.getMinefields(mcoords);
                for (int i = 0; i < minefields.size(); i++) {
                    Minefield mf = (Minefield) 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") + " " + owner; //$NON-NLS-1$ //$NON-NLS-2$
                            break;
                        case (Minefield.TYPE_THUNDER):
                            strings[stringsIndex] = mf.getName() + Messages.getString("BoardView1.minefield") + "(" + mf.getDamage() + ")" + owner; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                            break;
                        case (Minefield.TYPE_COMMAND_DETONATED):
                            strings[stringsIndex] = mf.getName() + Messages.getString("BoardView1.minefield") + " " + 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.getSetting() + ") " + owner; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                            } else {
                                strings[stringsIndex] = mf.getName() + Messages.getString("BoardView1.minefield") + " " + owner; //$NON-NLS-1$ //$NON-NLS-2$
                            }
                            break;
                        case (Minefield.TYPE_THUNDER_ACTIVE):
                            strings[stringsIndex] = mf.getName() + Messages.getString("BoardView1.minefield") + "(" + mf.getDamage() + ")" + owner; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                            break;
                        case (Minefield.TYPE_THUNDER_INFERNO):
                            strings[stringsIndex] = mf.getName() + Messages.getString("BoardView1.minefield") + "(" + mf.getDamage() + ")" + owner; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                            break;
                    }
                    stringsIndex++;
                }
            }
        }
        // check if it's on any entities
        for (Iterator i = entitySprites.iterator(); i.hasNext();) {
            final EntitySprite eSprite = (EntitySprite) i.next();
            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 (Iterator i = attackSprites.iterator(); i.hasNext();) {
            final AttackSprite aSprite = (AttackSprite) i.next();
            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 (Iterator i = artilleryAttacks.iterator(); i.hasNext();) {
            final ArtilleryAttackAction aaa = (ArtilleryAttackAction) i.next();
            final WeaponResult wr = aaa.getWR();
            final Entity ae = game.getEntity(wr.waa.getEntityId());
            String s = null;
            if (ae != null) {
                if (wr.waa.getWeaponId() > -1) {
                    Mounted weap = ae.getEquipment(wr.waa.getWeaponId());
                    s = weap.getName();
                    if (wr.waa.getAmmoId() > -1) {
                        Mounted ammo = ae.getEquipment(wr.waa.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), wr.toHit.getValueAsString()});
        }

        //check artillery fire adjustment
        if (curWeapon != null && selectedEntity != null) {
            //process targetted hexes
            int amod;
            //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)});
            }
        }

        StringBuffer buf = new StringBuffer();
        for (String aString : strings ) {
            buf.append(aString);
            buf.append('\n');
        }
        return buf.toString();
    }

    private void redrawMovingEntity(Entity entity, Coords position, int facing) {
        Integer entityId = new Integer(entity.getId());
        EntitySprite sprite = (EntitySprite) entitySpriteIds.get(entityId);
        ArrayList<Sprite> newSprites;
        HashMap<Integer, Sprite> newSpriteIds;
        if (sprite != null) {
            newSprites = new ArrayList<Sprite>(entitySprites);
            newSpriteIds = new HashMap<Integer, Sprite>(entitySpriteIds);
            newSprites.remove(sprite);
            entitySprites = newSprites;
            entitySpriteIds = newSpriteIds;
        }
        MovingEntitySprite mSprite =
                (MovingEntitySprite) movingEntitySpriteIds.get(entityId);
        newSprites = new ArrayList<Sprite>(movingEntitySprites);
        newSpriteIds = new HashMap<Integer, Sprite>(movingEntitySpriteIds);
        if (mSprite != null) {
            newSprites.remove(mSprite);
        }
        if (entity.getPosition() != null) {
            mSprite = new MovingEntitySprite(entity, position, facing);
            newSprites.add(mSprite);
            newSpriteIds.put(entityId, mSprite);
        }
        movingEntitySprites = newSprites;
        movingEntitySpriteIds = newSpriteIds;
    }

    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!
     * <p/>
     * Try to prevent annoying ConcurrentModificationExceptions
     */
    public void redrawEntity(Entity entity) {
        Integer entityId = new Integer(entity.getId());
        EntitySprite sprite = (EntitySprite) entitySpriteIds.get(entityId);
        ArrayList<Sprite> newSprites = new ArrayList<Sprite>(entitySprites);
        HashMap<Integer, Sprite> newSpriteIds = new HashMap<Integer, Sprite>(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 (Sprite spriteTemp : C3Sprites) {
            C3Sprite c3sprite = (C3Sprite) spriteTemp;
            if (c3sprite.entityId == entity.getId())
                C3Sprites.remove(c3sprite);
            else if (c3sprite.masterId == entity.getId()) {
                // Only redraw client-to-master; otherwise
                // we leave stray lines when we move.
                if (entity.hasC3()) {
                    C3Sprites.add(new C3Sprite(game.getEntity(c3sprite.entityId), game.getEntity(c3sprite.masterId)));
                }
                C3Sprites.remove(c3sprite);
            }
        }
        if (entity.hasC3() || entity.hasC3i()) addC3Link(entity);
        repaint(100);
    }

    /**
     * Clears all old entity sprites out of memory and sets up new ones.
     */
    private void redrawAllEntities() {
        ArrayList<Sprite> newSprites = new ArrayList<Sprite>(game.getNoOfEntities());
        HashMap<Integer, Sprite> newSpriteIds = new HashMap<Integer, Sprite>(game.getNoOfEntities());
        ArrayList<Sprite> newWrecks = new ArrayList<Sprite>();
        Enumeration e = game.getWreckedEntities();
        while (e.hasMoreElements()) {
            Entity entity = (Entity) e.nextElement();
            if (!(entity instanceof Infantry) && (entity.getPosition() != null)) {
                WreckSprite ws = new WreckSprite(entity);
                newWrecks.add(ws);
            }
        }
        clearC3Networks();
        for (Enumeration i = game.getEntities(); i.hasMoreElements();) {
            final Entity 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;
        repaint(100);
    }

    /**
     * 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 (c == null) 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(MovePath md) {
        ArrayList<Sprite> temp = pathSprites;
        MoveStep previousStep = null;
        clearMovementData();
        for (Enumeration i = md.getSteps(); i.hasMoreElements();) {
            final MoveStep step = (MoveStep) i.nextElement();
            // check old movement path for reusable step sprites
            boolean found = false;
            for (Iterator j = temp.iterator(); j.hasNext();) {
                final StepSprite sprite = (StepSprite) j.next();
                if (sprite.getStep().canReuseSprite(step)) {
                    pathSprites.add(sprite);
                    found = true;
                }
            }
            if (!found) {
                if (previousStep != null &&
                        (step.getType() == MovePath.STEP_UP ||
                        step.getType() == MovePath.STEP_DOWN) &&
                        (previousStep.getType() == MovePath.STEP_UP ||
                        previousStep.getType() == MovePath.STEP_DOWN)) {
                    //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;
                }
                pathSprites.add(new StepSprite(step));
            }
            previousStep = step;
        }
    }

    /**
     * Clears current movement data from the screen
     */
    public void clearMovementData() {
        ArrayList<Sprite> temp = pathSprites;
        pathSprites = new ArrayList<Sprite>();
        for (Iterator i = temp.iterator(); i.hasNext();) {
            final Sprite sprite = (Sprite) i.next();
            repaintBounds(sprite.getBounds());
        }
    }

    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.
     */
    private void addC3Link(Entity e) {
        if (e.getPosition() == null) return;
        if (e.hasC3i()) {
            for (Enumeration i = game.getEntities(); i.hasMoreElements();) {
                final Entity fe = (Entity) i.nextElement();
                if (fe.getPosition() == null) return;
                if (e.onSameC3NetworkAs(fe)) {
                    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 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) {
            return;
        }
        for (final Iterator i = attackSprites.iterator(); i.hasNext();) {
            final AttackSprite sprite = (AttackSprite) i.next();

            // 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();
                }
                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 void removeAttacksFor(int entityId) {
        // or rather, only keep sprites NOT for that entity
        ArrayList<Sprite> toKeep = new ArrayList<Sprite>(attackSprites.size());
        for (Iterator i = attackSprites.iterator(); i.hasNext();) {
            AttackSprite sprite = (AttackSprite) i.next();
            if (sprite.getEntityId() != entityId) {
                toKeep.add(sprite);
            }
        }
        attackSprites = toKeep;
    }

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

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

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

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

    private void secondLOSHex(Coords c2, Coords c1) {
        if (useLOSTool) {
            moveCursor(firstLOSSprite, c1);
            moveCursor(secondLOSSprite, c2);
            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;
            LosEffects le = LosEffects.calculateLos(game, ai);
            StringBuffer message = new StringBuffer();
            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()}));
            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) {
                    if (game.getOptions().booleanOption("maxtech_fire")) { //$NON-NLS-1$
                        message.append(Messages.getString("BoardView1.HeavySmoke", new Object[]{//$NON-NLS-1$
                            new Integer(le.getHeavySmoke())}));
                    } else {
                        message.append(Messages.getString("BoardView1.Smoke", 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$
                }
            }
            JOptionPane.showMessageDialog(frame, message.toString(), Messages.getString("BoardView1.LOSTitle"), JOptionPane.INFORMATION_MESSAGE);
        }
    }

    /**
     * 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()
     */
    private 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
     */
    private 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 (vScrollbar != null) {
            vScrollbar.setValue(scroll.y);
        }
        if (hScrollbar != null) {
            hScrollbar.setValue(scroll.x);
        }
    }

    void stopScrolling() {
        isScrolling = false;
    }

    /**
     * Initializes the various overlay polygons with their
     * vertices.
     */
    private 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);
    }

    private synchronized boolean doMoveUnits(long idleTime) {
        boolean movingSomething = false;
        if (movingUnits.size() > 0) {
            moveWait += idleTime;
            if (moveWait > GUIPreferences.getInstance().getInt("AdvancedMoveStepDelay")) {
                Vector spent = new Vector();
                for (int i = 0; i < movingUnits.size(); i++) {
                    Object[] move = (Object[]) movingUnits.elementAt(i);
                    Entity e = (Entity) move[0];
                    Vector movePath = (Vector) move[1];
                    movingSomething = true;
                    Entity ge = game.getEntity(e.getId());
                    if (movePath.size() > 0) {
                        UnitLocation loc =
                                ((UnitLocation) movePath.elementAt(0));
                        if (ge != null) {
                            redrawMovingEntity(e,
                                    loc.getCoords(),
                                    loc.getFacing());
                        }
                        movePath.removeElementAt(0);
                    } else {
                        if (ge != null) {
                            redrawEntity(ge);
                        }
                        spent.addElement(move);
                    }
                }
                for (int i = 0; i < spent.size(); i++) {
                    Object[] move = (Object[]) spent.elementAt(i);
                    movingUnits.removeElement(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 -= 36;
                scroll.x -= 36;
                break;
            case KeyEvent.VK_NUMPAD8:
                scroll.y -= 36;
                break;
            case KeyEvent.VK_NUMPAD9:
                scroll.y -= 36;
                scroll.x += 36;
                break;
            case KeyEvent.VK_NUMPAD1:
                scroll.y += 36;
                scroll.x -= 36;
                break;
            case KeyEvent.VK_NUMPAD2:
                scroll.y += 36;
                break;
            case KeyEvent.VK_NUMPAD3:
                scroll.y += 36;
                scroll.x += 36;
                break;
            case KeyEvent.VK_NUMPAD4:
                scroll.x -= 36;
                break;
            case KeyEvent.VK_NUMPAD6:
                scroll.x += 36;
                break;
            case KeyEvent.VK_NUMPAD5:
                // center on the selected entity
                Vector v = game.getPlayerEntities(localPlayer);
                //TODO please eliminate the usage of the clientgui
                Entity se = clientgui == null ? null : game.getEntity(clientgui.getSelectedEntityNum());
                for (int i = 0; i < v.size(); i++) {
                    Entity e = (Entity) v.elementAt(i);
                    if (e.equals(se)) {
                        centerOnHex(e.getPosition());
                    }
                }
                break;
            case KeyEvent.VK_CONTROL:
                ctlKeyHeld = true;
                initCtlScroll = true;
                break;
        }

        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 (point == null) {
            return;
        }
        oldMousePosition = point;
        isTipPossible = false;
        for (int i = 0; i < displayables.size(); i++) {
            Displayable disp = (Displayable) displayables.elementAt(i);
            if ((backSize != null) && (disp.isHit(point, backSize))) {
                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_FIRING) {
            // 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
        }

        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++) {
            Displayable disp = (Displayable) displayables.elementAt(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.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) {
    }

    //
    // MouseMotionListener
    //
    public void mouseDragged(MouseEvent me) {
        isTipPossible = false;
        Point point = me.getPoint();
        if (point == null) {
            return;
        }
        for (int i = 0; i < displayables.size(); i++) {
            Displayable disp = (Displayable) displayables.elementAt(i);
            if (disp.isDragged(point, 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_FIRING) {
            // 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 (point == null) {
            return;
        }
        for (int i = 0; i < displayables.size(); i++) {
            Displayable disp = (Displayable) displayables.elementAt(i);
            if (disp.isBeingDragged()) {
                isTipPossible = false;
                return;
            }
            if (backSize != null) {
                disp.isMouseOver(point, backSize);
            }
        }
        mousePos = point;

        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();
    }

    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();
        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;
        }
    }

    /**
     * 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.
     */
    private abstract class Sprite implements ImageObserver {
        Rectangle bounds;
        Image image;
        //Set this to true if you don't want the sprite to be drawn.
        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 final 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 final 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();
            }
        }

    }

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

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

            // start offscreen
            setOffScreen();
        }

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

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

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

        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()));
            this.bounds = tempBounds;
            this.image = null;
        }

        /**
         * Creates the sprite for this entity.  It is an extra pain to
         * create transparent images in AWT.
         */
        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();
        }

        public Rectangle getBounds() {
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));
            this.bounds = tempBounds;
            return bounds;
        }

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

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

        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.
         */
        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 final class WreckSprite extends Sprite {
        private final Entity entity;
        private final Rectangle modelRect;

        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()));
            this.bounds = tempBounds;
            this.image = null;
        }

        public Rectangle getBounds() {
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));
            this.bounds = tempBounds;
            return bounds;
        }

        /**
         * Creates the sprite for this entity.  It is an extra pain to
         * create transparent images in AWT.
         */
        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 (wreck != null) {
                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();
        }

    }

    /**
     * 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 final class EntitySprite extends Sprite {
        private final Entity entity;
        private Rectangle entityRect;
        private final Rectangle modelRect;

        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(")");
            }
            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()));
            this.bounds = tempBounds;
            this.entityRect = new Rectangle(bounds.x + (int) (20 * scale),
                    bounds.y + (int) (14 * scale),
                    (int) (44 * scale),
                    (int) (44 * scale));
            this.image = null;
        }

        public Rectangle getBounds() {
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));
            this.bounds = tempBounds;
            this.entityRect = new Rectangle(bounds.x + (int) (20 * scale),
                    bounds.y + (int) (14 * scale),
                    (int) (44 * scale),
                    (int) (44 * scale));
            return bounds;
        }

        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            if (trackThisEntitiesVisibilityInfo(this.entity)
                    && !this.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.
         */
        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$
            }
            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 entity image
            graph.drawImage(tileManager.imageFor(entity), 0, 0, this);

            // draw box with shortName
            Color text;
            Color bkgd;
            Color 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)) {
                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;
            boolean ge = false;
            if (entity instanceof Tank) {
                turretLocked =
                        !((Tank) entity).hasNoTurret() &&
                        !entity.canChangeSecondaryFacing();
            } else if (entity instanceof GunEmplacement) {
                turretLocked =
                        ((GunEmplacement) entity).hasTurret() &&
                        !entity.canChangeSecondaryFacing();
                ge = true;
            }

            // draw condition strings
            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 (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 (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.getSwarmAttackerId() != Entity.NONE) {
                // 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("D", 40, 71); //$NON-NLS-1$
                graph.setColor(Color.black);
                graph.drawString("D", 39, 70); //$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;
            double percentRemaining;
            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.
         */
        private boolean isInside(Point point) {
            return entityRect.contains(point.x + view.x - offset.x,
                    point.y + view.y - offset.y);
        }

        private 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();
            if (numAdv > 0) {
                buffer.append(" <")//$NON-NLS-1$
                        .append(numAdv)
                        .append(Messages.getString("BoardView1.advs")); //$NON-NLS-1$
            }
            tipStrings[0] = buffer.toString();
            GunEmplacement ge = null;
            if (entity instanceof GunEmplacement) {
                ge = (GunEmplacement) entity;
            }
            buffer = new StringBuffer();
            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(" (+")//$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("BoardBiew1.DFA1")); //$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 final class StepSprite extends Sprite {
        private final MoveStep step;

        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);
            this.image = null;
        }

        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:
                    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;
            }

            // 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:
                    // 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);
                    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);
                    break;
                case MovePath.STEP_GET_UP:
                case MovePath.STEP_UP:
                    // draw arrow indicating standing up
                    // also doubles as the climb indication
                    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);
                    break;
                case MovePath.STEP_CLIMB_MODE_ON:
                    // draw climb mode indicator
                    String 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 = 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:
                    // 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);
                    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_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;
                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();
        }

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

        public MoveStep getStep() {
            return step;
        }

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

            // If the step is using a road bonus, mark it.
            if (step.isPavementStep() && 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 final class C3Sprite extends Sprite {
        private Polygon C3Poly;
        final int entityId;
        final int masterId;
        final Entity entityE;
        final Entity entityM;
        final Color spriteColor;

        C3Sprite(Entity e, Entity m) {
            this.entityE = e;
            this.entityM = m;
            this.entityId = e.getId();
            this.masterId = m.getId();
            this.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);
                this.bounds = new Rectangle(C3Poly.getBounds());
                bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);
                this.image = null;
                return;
            }
            makePoly();

            // set bounds
            this.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
            this.image = null;
        }

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

        public Rectangle getBounds() {
            makePoly();
            // set bounds
            this.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);
            this.image = null;
            return bounds;
        }

        public boolean isReady() {
            return true;
        }

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

    }

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

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

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

            // set bounds
            this.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();
            }

            // nullify image
            this.image = null;
        }

        private void makePoly() {
            // make a polygon
            this.a = getHexLocation(ae.getPosition());
            this.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);
            }
        }

        public Rectangle getBounds() {
            makePoly();
            // set bounds
            this.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
            this.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 (final Iterator i = attackSprites.iterator(); i.hasNext();) {
                final AttackSprite sprite = (AttackSprite) i.next();
                if (sprite.getEntityId() == this.targetId && sprite.getTargetId() == this.entityId) {
                    sprite.rebuildToHalvedPolygon();
                    return true;
                }
            }
            return false;
        }

        public void prepare() {
        }

        public boolean isReady() {
            return true;
        }

        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
         */
        private 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.addElement(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.addElement(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.addElement(bufer);
        }

        public void addWeapon(PushAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            weaponDescs.addElement(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.addElement(Messages.getString("BoardView1.hit", new Object[]{club, roll})); //$NON-NLS-1$
        }

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

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

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

        public void addWeapon() {
            weaponDescs.addElement(Messages.getString("BoardView1.Searchlight"));
        }

        private 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 i = weaponDescs.iterator(); i.hasNext();) {
                tipStrings[tip++] = (String) 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.
     */
    private boolean isTileImagesLoaded() {
        return this.tileManager.isLoaded();
    }

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

    public TilesetManager getTilesetManager() {
        return tileManager;
    }

    // added by kenn
    public void drawRuler(Coords s, Coords e, Color sc, Color ec) {
        rulerStart = s;
        rulerEnd = e;
        rulerStartColor = sc;
        rulerEndColor = ec;
        repaint();
    }
    // end kenn

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

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

    //TODO Is there a better solution?
    //This is required because the BoardView creates the redraw thread 
    //that must be stopped explicitly     
    public void die() {
        redrawWorker.stop();
    }

    private final GameListener gameListener = new GameListenerAdapter() {
        public void gameEntityNew(GameEntityNewEvent e) {
            updateEcmList();
            redrawAllEntities();
        }

        public void gameEntityRemove(GameEntityRemoveEvent e) {
            updateEcmList();
            redrawAllEntities();
        }

        public void gameEntityChange(GameEntityChangeEvent e) {
            Vector mp = e.getMovePath();
            updateEcmList();
            if (mp != null && mp.size() > 0 && GUIPreferences.getInstance().getShowMoveStep()) {
                addMovingUnit(e.getEntity(), mp);
            } else {
                redrawEntity(e.getEntity());
            }
        }

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

        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();
        }

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

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

    private synchronized void boardChanged() {
//        boardImage = null;
//        boardGraph = null;
        redrawAllEntities();
    }

    private void clearSprites() {
        pathSprites = new ArrayList<Sprite>();
        attackSprites = new ArrayList<Sprite>();
        C3Sprites = new ArrayList<Sprite>();
    }

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

    /*
     * It's not quite polished solution, but on other hand it's better then nothing.
     */
    final class RedrawWorker implements Runnable {
        private boolean finished = false;

        public void start() {
            Thread thread = new Thread(this, "BoardView RedrawWorker Thread"); //$NON-NLS-1$
            thread.start();
        }

        public void stop() {
            finished = true;
        }

        public void run() {
            long lastTime = System.currentTimeMillis();
            long currentTime;
            while (!finished) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException ex) {
                    // duh?
                }
                if (finished) {
                    break;
                }
                if (!isShowing()) {
                    currentTime = System.currentTimeMillis();
                    lastTime = currentTime;
                    continue;
                }
                currentTime = System.currentTimeMillis();
                boolean redraw = false;
                for (int i = 0; i < displayables.size(); i++) {
                    Displayable disp = (Displayable) displayables.elementAt(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();
                } else {
                    repaint(100);
                }
                if (redraw) {
                    repaint();
                }
                lastTime = currentTime;
            }
        }
    }

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

    private static final class EcmBubble extends Coords {
        final int range;
        final int tint;

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

    //This is expensive, so precalculate when entity changes
    private void updateEcmList() {
        ArrayList<EcmBubble> list = new ArrayList<EcmBubble>();
        for (Enumeration e = game.getEntities(); e.hasMoreElements();) {
            Entity ent = (Entity) e.nextElement();
            if (ent.getPosition() == null || !ent.isDeployed() || ent.isOffBoard()) {
                continue;
            }
            int range = ent.getECMRange();
            if (range != Entity.NONE) {
                int tint = PlayerColors.getColorRGB(ent.getOwner().getColorIndex());
                list.add(new EcmBubble(ent.getPosition(), range, tint));
            }
        }
        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) {
                        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);
    }

}
