/* * $Id$ * * Copyright (c) 2000-2012 by Rodney Kinney, Joel Uckelman, Brent Easton * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.build.module; import java.awt.AWTEventMulticaster; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Component; import java.awt.Composite; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.Window; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; 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.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLayeredPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JToolBar; import javax.swing.KeyStroke; import javax.swing.OverlayLayout; import javax.swing.RootPaneContainer; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import net.miginfocom.swing.MigLayout; import org.jdesktop.animation.timing.Animator; import org.jdesktop.animation.timing.TimingTargetAdapter; import org.w3c.dom.Element; import VASSAL.build.AbstractConfigurable; import VASSAL.build.AutoConfigurable; import VASSAL.build.Buildable; import VASSAL.build.Configurable; import VASSAL.build.GameModule; import VASSAL.build.IllegalBuildException; import VASSAL.build.module.documentation.HelpFile; import VASSAL.build.module.map.BoardPicker; import VASSAL.build.module.map.CounterDetailViewer; import VASSAL.build.module.map.DefaultPieceCollection; import VASSAL.build.module.map.DrawPile; import VASSAL.build.module.map.Drawable; import VASSAL.build.module.map.ForwardToChatter; import VASSAL.build.module.map.ForwardToKeyBuffer; import VASSAL.build.module.map.GlobalMap; import VASSAL.build.module.map.HidePiecesButton; import VASSAL.build.module.map.HighlightLastMoved; import VASSAL.build.module.map.ImageSaver; import VASSAL.build.module.map.KeyBufferer; import VASSAL.build.module.map.LOS_Thread; import VASSAL.build.module.map.LayeredPieceCollection; import VASSAL.build.module.map.MapCenterer; import VASSAL.build.module.map.MapShader; import VASSAL.build.module.map.MassKeyCommand; import VASSAL.build.module.map.MenuDisplayer; import VASSAL.build.module.map.PieceCollection; import VASSAL.build.module.map.PieceMover; import VASSAL.build.module.map.PieceRecenterer; import VASSAL.build.module.map.Scroller; import VASSAL.build.module.map.SelectionHighlighters; import VASSAL.build.module.map.SetupStack; import VASSAL.build.module.map.StackExpander; import VASSAL.build.module.map.StackMetrics; import VASSAL.build.module.map.TextSaver; import VASSAL.build.module.map.Zoomer; import VASSAL.build.module.map.boardPicker.Board; import VASSAL.build.module.map.boardPicker.board.MapGrid; import VASSAL.build.module.map.boardPicker.board.Region; import VASSAL.build.module.map.boardPicker.board.RegionGrid; import VASSAL.build.module.map.boardPicker.board.ZonedGrid; import VASSAL.build.module.map.boardPicker.board.mapgrid.Zone; import VASSAL.build.module.properties.ChangePropertyCommandEncoder; import VASSAL.build.module.properties.GlobalProperties; import VASSAL.build.module.properties.MutablePropertiesContainer; import VASSAL.build.module.properties.MutableProperty; import VASSAL.build.module.properties.PropertySource; import VASSAL.build.widget.MapWidget; import VASSAL.command.AddPiece; import VASSAL.command.Command; import VASSAL.command.MoveTracker; import VASSAL.configure.BooleanConfigurer; import VASSAL.configure.ColorConfigurer; import VASSAL.configure.CompoundValidityChecker; import VASSAL.configure.Configurer; import VASSAL.configure.ConfigurerFactory; import VASSAL.configure.IconConfigurer; import VASSAL.configure.IntConfigurer; import VASSAL.configure.MandatoryComponent; import VASSAL.configure.NamedHotKeyConfigurer; import VASSAL.configure.PlayerIdFormattedStringConfigurer; import VASSAL.configure.VisibilityCondition; import VASSAL.counters.ColoredBorder; import VASSAL.counters.Deck; import VASSAL.counters.DeckVisitor; import VASSAL.counters.DeckVisitorDispatcher; import VASSAL.counters.DragBuffer; import VASSAL.counters.GamePiece; import VASSAL.counters.Highlighter; import VASSAL.counters.KeyBuffer; import VASSAL.counters.PieceFinder; import VASSAL.counters.PieceVisitorDispatcher; import VASSAL.counters.Properties; import VASSAL.counters.ReportState; import VASSAL.counters.Stack; import VASSAL.i18n.Resources; import VASSAL.i18n.TranslatableConfigurerFactory; import VASSAL.preferences.PositionOption; import VASSAL.preferences.Prefs; import VASSAL.tools.AdjustableSpeedScrollPane; import VASSAL.tools.ComponentSplitter; import VASSAL.tools.KeyStrokeSource; import VASSAL.tools.LaunchButton; import VASSAL.tools.NamedKeyStroke; import VASSAL.tools.ToolBarComponent; import VASSAL.tools.UniqueIdManager; import VASSAL.tools.WrapLayout; import VASSAL.tools.menu.MenuManager; /* import org.jdesktop.swinghelper.layer.JXLayer; import org.jdesktop.swinghelper.layer.demo.DebugPainter; */ /** * The Map is the main component for displaying and containing {@link GamePiece}s during play. Pieces are displayed on * a Map and moved by clicking and dragging. Keyboard events are forwarded to selected pieces. Multiple map windows are * supported in a single game, with dragging between windows allowed. * * A Map may contain many different {@link Buildable} subcomponents. Components which are added directly to a Map are * contained in the <code>VASSAL.build.module.map</code> package */ public class Map extends AbstractConfigurable implements GameComponent, MouseListener, MouseMotionListener, DropTargetListener, Configurable, UniqueIdManager.Identifyable, ToolBarComponent, MutablePropertiesContainer, PropertySource, PlayerRoster.SideChangeListener { protected static boolean changeReportingEnabled = true; protected String mapID = ""; //$NON-NLS-1$ protected String mapName = ""; //$NON-NLS-1$ protected static final String MAIN_WINDOW_HEIGHT = "mainWindowHeight"; //$NON-NLS-1$ protected static UniqueIdManager idMgr = new UniqueIdManager("Map"); //$NON-NLS-1$ protected JPanel theMap; protected ArrayList<Drawable> drawComponents = new ArrayList<Drawable>(); protected JLayeredPane layeredPane = new JLayeredPane(); protected JScrollPane scroll; protected ComponentSplitter.SplitPane mainWindowDock; protected BoardPicker picker; protected JToolBar toolBar = new JToolBar(); protected Zoomer zoom; protected StackMetrics metrics; protected Dimension edgeBuffer = new Dimension(0, 0); protected Color bgColor = Color.white; protected LaunchButton launchButton; protected boolean useLaunchButton = false; protected boolean useLaunchButtonEdit = false; protected String markMovedOption = GlobalOptions.ALWAYS; protected String markUnmovedIcon = "/images/unmoved.gif"; //$NON-NLS-1$ protected String markUnmovedText = ""; //$NON-NLS-1$ protected String markUnmovedTooltip = Resources.getString("Map.mark_unmoved"); //$NON-NLS-1$ protected MouseListener multicaster = null; protected ArrayList<MouseListener> mouseListenerStack = new ArrayList<MouseListener>(); protected List<Board> boards = new CopyOnWriteArrayList<Board>(); protected int[][] boardWidths; // Cache of board widths by row/column protected int[][] boardHeights; // Cache of board heights by row/column protected PieceCollection pieces = new DefaultPieceCollection(); protected Highlighter highlighter = new ColoredBorder(); protected ArrayList<Highlighter> highlighters = new ArrayList<Highlighter>(); protected boolean clearFirst = false; // Whether to clear the display before // drawing the map protected boolean hideCounters = false; // Option to hide counters to see // map protected float pieceOpacity = 1.0f; protected boolean allowMultiple = false; protected VisibilityCondition visibilityCondition; protected DragGestureListener dragGestureListener; protected String moveWithinFormat; protected String moveToFormat; protected String createFormat; protected String changeFormat = "$" + MESSAGE + "$"; //$NON-NLS-1$ //$NON-NLS-2$ protected NamedKeyStroke moveKey; protected PropertyChangeListener globalPropertyListener; protected String tooltip = ""; //$NON-NLS-1$ protected MutablePropertiesContainer propsContainer = new MutablePropertiesContainer.Impl(); protected PropertyChangeListener repaintOnPropertyChange = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { repaint(); } }; protected PieceMover pieceMover; protected KeyListener[] saveKeyListeners = null; public Map() { getView(); theMap.addMouseListener(this); if (shouldDockIntoMainWindow()) { toolBar.setLayout(new MigLayout("ins 0,gapx 0,hidemode 3")); } else { toolBar.setLayout(new WrapLayout(WrapLayout.LEFT, 0, 0)); } toolBar.setAlignmentX(0.0F); toolBar.setFloatable(false); } // Global Change Reporting control public static void setChangeReportingEnabled(boolean b) { changeReportingEnabled = b; } public static boolean isChangeReportingEnabled() { return changeReportingEnabled; } public static final String NAME = "mapName"; //$NON-NLS-1$ public static final String MARK_MOVED = "markMoved"; //$NON-NLS-1$ public static final String MARK_UNMOVED_ICON = "markUnmovedIcon"; //$NON-NLS-1$ public static final String MARK_UNMOVED_TEXT = "markUnmovedText"; //$NON-NLS-1$ public static final String MARK_UNMOVED_TOOLTIP = "markUnmovedTooltip"; //$NON-NLS-1$ public static final String EDGE_WIDTH = "edgeWidth"; //$NON-NLS-1$ public static final String EDGE_HEIGHT = "edgeHeight"; //$NON-NLS-1$ public static final String BACKGROUND_COLOR = "backgroundcolor"; public static final String HIGHLIGHT_COLOR = "color"; //$NON-NLS-1$ public static final String HIGHLIGHT_THICKNESS = "thickness"; //$NON-NLS-1$ public static final String ALLOW_MULTIPLE = "allowMultiple"; //$NON-NLS-1$ public static final String USE_LAUNCH_BUTTON = "launch"; //$NON-NLS-1$ public static final String BUTTON_NAME = "buttonName"; //$NON-NLS-1$ public static final String TOOLTIP = "tooltip"; //$NON-NLS-1$ public static final String ICON = "icon"; //$NON-NLS-1$ public static final String HOTKEY = "hotkey"; //$NON-NLS-1$ public static final String SUPPRESS_AUTO = "suppressAuto"; //$NON-NLS-1$ public static final String MOVE_WITHIN_FORMAT = "moveWithinFormat"; //$NON-NLS-1$ public static final String MOVE_TO_FORMAT = "moveToFormat"; //$NON-NLS-1$ public static final String CREATE_FORMAT = "createFormat"; //$NON-NLS-1$ public static final String CHANGE_FORMAT = "changeFormat"; //$NON-NLS-1$ public static final String MOVE_KEY = "moveKey"; //$NON-NLS-1$ public static final String MOVING_STACKS_PICKUP_UNITS = "movingStacksPickupUnits"; //$NON-NLS-1$ public void setAttribute(String key, Object value) { if (NAME.equals(key)) { setMapName((String) value); } else if (MARK_MOVED.equals(key)) { markMovedOption = (String) value; } else if (MARK_UNMOVED_ICON.equals(key)) { markUnmovedIcon = (String) value; if (pieceMover != null) { pieceMover.setAttribute(key, value); } } else if (MARK_UNMOVED_TEXT.equals(key)) { markUnmovedText = (String) value; if (pieceMover != null) { pieceMover.setAttribute(key, value); } } else if (MARK_UNMOVED_TOOLTIP.equals(key)) { markUnmovedTooltip = (String) value; } else if ("edge".equals(key)) { // Backward-compatible //$NON-NLS-1$ String s = (String) value; int i = s.indexOf(','); //$NON-NLS-1$ if (i > 0) { edgeBuffer = new Dimension(Integer.parseInt(s.substring(0, i)), Integer.parseInt(s.substring(i + 1))); } } else if (EDGE_WIDTH.equals(key)) { if (value instanceof String) { value = Integer.valueOf((String) value); } try { edgeBuffer = new Dimension(((Integer) value).intValue(), edgeBuffer.height); } catch (NumberFormatException ex) { throw new IllegalBuildException(ex); } } else if (EDGE_HEIGHT.equals(key)) { if (value instanceof String) { value = Integer.valueOf((String) value); } try { edgeBuffer = new Dimension(edgeBuffer.width, ((Integer) value).intValue()); } catch (NumberFormatException ex) { throw new IllegalBuildException(ex); } } else if (BACKGROUND_COLOR.equals(key)) { if (value instanceof String) { value = ColorConfigurer.stringToColor((String)value); } bgColor = (Color)value; } else if (ALLOW_MULTIPLE.equals(key)) { if (value instanceof String) { value = Boolean.valueOf((String) value); } allowMultiple = ((Boolean) value).booleanValue(); if (picker != null) { picker.setAllowMultiple(allowMultiple); } } else if (HIGHLIGHT_COLOR.equals(key)) { if (value instanceof String) { value = ColorConfigurer.stringToColor((String) value); } if (value != null) { ((ColoredBorder) highlighter).setColor((Color) value); } } else if (HIGHLIGHT_THICKNESS.equals(key)) { if (value instanceof String) { value = Integer.valueOf((String) value); } if (highlighter instanceof ColoredBorder) { ((ColoredBorder) highlighter).setThickness(((Integer) value).intValue()); } } else if (USE_LAUNCH_BUTTON.equals(key)) { if (value instanceof String) { value = Boolean.valueOf((String) value); } useLaunchButtonEdit = ((Boolean) value).booleanValue(); launchButton.setVisible(useLaunchButton); } else if (SUPPRESS_AUTO.equals(key)) { if (value instanceof String) { value = Boolean.valueOf((String) value); } if (Boolean.TRUE.equals(value)) { moveWithinFormat = ""; //$NON-NLS-1$ } } else if (MOVE_WITHIN_FORMAT.equals(key)) { moveWithinFormat = (String) value; } else if (MOVE_TO_FORMAT.equals(key)) { moveToFormat = (String) value; } else if (CREATE_FORMAT.equals(key)) { createFormat = (String) value; } else if (CHANGE_FORMAT.equals(key)) { changeFormat = (String) value; } else if (MOVE_KEY.equals(key)) { if (value instanceof String) { value = NamedHotKeyConfigurer.decode((String) value); } moveKey = (NamedKeyStroke) value; } else if (TOOLTIP.equals(key)) { tooltip = (String) value; launchButton.setAttribute(key, value); } else { launchButton.setAttribute(key, value); } } public String getAttributeValueString(String key) { if (NAME.equals(key)) { return getMapName(); } else if (MARK_MOVED.equals(key)) { return markMovedOption; } else if (MARK_UNMOVED_ICON.equals(key)) { return markUnmovedIcon; } else if (MARK_UNMOVED_TEXT.equals(key)) { return markUnmovedText; } else if (MARK_UNMOVED_TOOLTIP.equals(key)) { return markUnmovedTooltip; } else if (EDGE_WIDTH.equals(key)) { return String.valueOf(edgeBuffer.width); //$NON-NLS-1$ } else if (EDGE_HEIGHT.equals(key)) { return String.valueOf(edgeBuffer.height); //$NON-NLS-1$ } else if (BACKGROUND_COLOR.equals(key)) { return ColorConfigurer.colorToString(bgColor); } else if (ALLOW_MULTIPLE.equals(key)) { return String.valueOf(picker.isAllowMultiple()); //$NON-NLS-1$ } else if (HIGHLIGHT_COLOR.equals(key)) { if (highlighter instanceof ColoredBorder) { return ColorConfigurer.colorToString( ((ColoredBorder) highlighter).getColor()); } else { return null; } } else if (HIGHLIGHT_THICKNESS.equals(key)) { if (highlighter instanceof ColoredBorder) { return String.valueOf( ((ColoredBorder) highlighter).getThickness()); //$NON-NLS-1$ } else { return null; } } else if (USE_LAUNCH_BUTTON.equals(key)) { return String.valueOf(useLaunchButtonEdit); } else if (MOVE_WITHIN_FORMAT.equals(key)) { return getMoveWithinFormat(); } else if (MOVE_TO_FORMAT.equals(key)) { return getMoveToFormat(); } else if (CREATE_FORMAT.equals(key)) { return getCreateFormat(); } else if (CHANGE_FORMAT.equals(key)) { return getChangeFormat(); } else if (MOVE_KEY.equals(key)) { return NamedHotKeyConfigurer.encode(moveKey); } else if (TOOLTIP.equals(key)) { return (tooltip == null || tooltip.length() == 0) ? launchButton.getAttributeValueString(name) : tooltip; } else { return launchButton.getAttributeValueString(key); } } public void build(Element e) { ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { if (mainWindowDock == null && launchButton.isEnabled() && theMap.getTopLevelAncestor() != null) { theMap.getTopLevelAncestor().setVisible(!theMap.getTopLevelAncestor().isVisible()); } } }; launchButton = new LaunchButton(Resources.getString("Editor.Map.map"), TOOLTIP, BUTTON_NAME, HOTKEY, ICON, al); launchButton.setEnabled(false); launchButton.setVisible(false); if (e != null) { super.build(e); getBoardPicker(); getStackMetrics(); } else { getBoardPicker(); getStackMetrics(); addChild(new ForwardToKeyBuffer()); addChild(new Scroller()); addChild(new ForwardToChatter()); addChild(new MenuDisplayer()); addChild(new MapCenterer()); addChild(new StackExpander()); addChild(new PieceMover()); addChild(new KeyBufferer()); addChild(new ImageSaver()); addChild(new CounterDetailViewer()); setMapName("Main Map"); } if (getComponentsOf(GlobalProperties.class).isEmpty()) { addChild(new GlobalProperties()); } if (getComponentsOf(SelectionHighlighters.class).isEmpty()) { addChild(new SelectionHighlighters()); } if (getComponentsOf(HighlightLastMoved.class).isEmpty()) { addChild(new HighlightLastMoved()); } setup(false); } private void addChild(Buildable b) { add(b); b.addTo(this); } /** * Every map must include a {@link BoardPicker} as one of its build components */ public void setBoardPicker(BoardPicker picker) { if (this.picker != null) { GameModule.getGameModule().removeCommandEncoder(picker); GameModule.getGameModule().getGameState().addGameComponent(picker); } this.picker = picker; if (picker != null) { picker.setAllowMultiple(allowMultiple); GameModule.getGameModule().addCommandEncoder(picker); GameModule.getGameModule().getGameState().addGameComponent(picker); } } /** * Every map must include a {@link BoardPicker} as one of its build components * * @return the BoardPicker for this map */ public BoardPicker getBoardPicker() { if (picker == null) { picker = new BoardPicker(); picker.build(null); add(picker); picker.addTo(this); } return picker; } /** * A map may include a {@link Zoomer} as one of its build components */ public void setZoomer(Zoomer z) { zoom = z; } /** * A map may include a {@link Zoomer} as one of its build components * * @return the Zoomer for this map */ public Zoomer getZoomer() { return zoom; } /** * Every map must include a {@link StackMetrics} as one of its build components, which governs the stacking behavior * of GamePieces on the map */ public void setStackMetrics(StackMetrics sm) { metrics = sm; } /** * Every map must include a {@link StackMetrics} as one of its build * components, which governs the stacking behavior of GamePieces on the map * * @return the StackMetrics for this map */ public StackMetrics getStackMetrics() { if (metrics == null) { metrics = new StackMetrics(); metrics.build(null); add(metrics); metrics.addTo(this); } return metrics; } /** * @return the current zoom factor for the map */ public double getZoom() { return zoom == null ? 1.0 : zoom.getZoomFactor(); } /** * @return the toolbar for this map's window */ public JToolBar getToolBar() { return toolBar; } /** * Add a {@link Drawable} component to this map * * @see #paint */ public void addDrawComponent(Drawable theComponent) { drawComponents.add(theComponent); } /** * Remove a {@link Drawable} component from this map * * @see #paint */ public void removeDrawComponent(Drawable theComponent) { drawComponents.remove(theComponent); } /** * Expects to be added to a {@link GameModule}. Determines a unique id for * this Map. Registers itself as {@link KeyStrokeSource}. Registers itself * as a {@link GameComponent}. Registers itself as a drop target and drag * source. * * @see #getId * @see DragBuffer */ public void addTo(Buildable b) { useLaunchButton = useLaunchButtonEdit; idMgr.add(this); final GameModule g = GameModule.getGameModule(); g.addCommandEncoder(new ChangePropertyCommandEncoder(this)); validator = new CompoundValidityChecker( new MandatoryComponent(this, BoardPicker.class), new MandatoryComponent(this, StackMetrics.class)).append(idMgr); final DragGestureListener dgl = new DragGestureListener() { public void dragGestureRecognized(DragGestureEvent dge) { if (mouseListenerStack.isEmpty() && dragGestureListener != null) { dragGestureListener.dragGestureRecognized(dge); } } }; DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer( theMap, DnDConstants.ACTION_MOVE, dgl); theMap.setDropTarget(PieceMover.DragHandler.makeDropTarget( theMap, DnDConstants.ACTION_MOVE, this)); g.getGameState().addGameComponent(this); g.getToolBar().add(launchButton); if (shouldDockIntoMainWindow()) { final IntConfigurer config = new IntConfigurer(MAIN_WINDOW_HEIGHT, null, -1); Prefs.getGlobalPrefs().addOption(null, config); final ComponentSplitter splitter = new ComponentSplitter(); /* final JXLayer<JComponent> jxl = new JXLayer<JComponent>(layeredPane); final DebugPainter<JComponent> dp = new DebugPainter<JComponent>(); jxl.setPainter(dp); mainWindowDock = splitter.splitBottom(splitter.getSplitAncestor(GameModule.getGameModule().getControlPanel(), -1), jxl, true); */ mainWindowDock = splitter.splitBottom( splitter.getSplitAncestor(g.getControlPanel(), -1), layeredPane, true ); g.addKeyStrokeSource( new KeyStrokeSource(theMap, JComponent.WHEN_FOCUSED)); } else { g.addKeyStrokeSource( new KeyStrokeSource(theMap, JComponent.WHEN_IN_FOCUSED_WINDOW)); } // Fix for bug 1630993: toolbar buttons not appearing toolBar.addHierarchyListener(new HierarchyListener() { public void hierarchyChanged(HierarchyEvent e) { Window w; if ((w = SwingUtilities.getWindowAncestor(toolBar)) != null) { w.validate(); } if (toolBar.getSize().width > 0) { toolBar.removeHierarchyListener(this); } } }); PlayerRoster.addSideChangeListener(this); g.getPrefs().addOption( Resources.getString("Prefs.general_tab"), //$NON-NLS-1$ new IntConfigurer( PREFERRED_EDGE_DELAY, Resources.getString("Map.scroll_delay_preference"), //$NON-NLS-1$ PREFERRED_EDGE_SCROLL_DELAY ) ); g.getPrefs().addOption( Resources.getString("Prefs.general_tab"), //$NON-NLS-1$ new BooleanConfigurer( MOVING_STACKS_PICKUP_UNITS, Resources.getString("Map.moving_stacks_preference"), //$NON-NLS-1$ Boolean.FALSE ) ); } public void setPieceMover(PieceMover mover) { pieceMover = mover; } public void removeFrom(Buildable b) { GameModule.getGameModule().getGameState().removeGameComponent(this); Window w = SwingUtilities.getWindowAncestor(theMap); if (w != null) { w.dispose(); } GameModule.getGameModule().getToolBar().remove(launchButton); idMgr.remove(this); if (picker != null) { GameModule.getGameModule().removeCommandEncoder(picker); GameModule.getGameModule().getGameState().addGameComponent(picker); } PlayerRoster.removeSideChangeListener(this); } public void sideChanged(String oldSide, String newSide) { repaint(); } /** * Set the boards for this map. Each map may contain more than one * {@link Board}. */ public synchronized void setBoards(Collection<Board> c) { boards.clear(); for (Board b : c) { b.setMap(this); boards.add(b); } setBoardBoundaries(); } /** * Set the boards for this map. Each map may contain more than one * {@link Board}. * @deprecated Use {@link #setBoards(Collection<Board>)} instead. */ @Deprecated public synchronized void setBoards(Enumeration<Board> boardList) { setBoards(Collections.list(boardList)); } public Command getRestoreCommand() { return null; } /** * @return the {@link Board} on this map containing the argument point */ public Board findBoard(Point p) { for (Board b : boards) { if (b.bounds().contains(p)) return b; } return null; } /** * * @return the {@link Zone} on this map containing the argument point */ public Zone findZone(Point p) { Board b = findBoard(p); if (b != null) { MapGrid grid = b.getGrid(); if (grid != null && grid instanceof ZonedGrid) { Rectangle r = b.bounds(); p.translate(-r.x, -r.y); // Translate to Board co-ords return ((ZonedGrid) grid).findZone(p); } } return null; } /** * Search on all boards for a Zone with the given name * @param Zone name * @return Located zone */ public Zone findZone(String name) { for (Board b : boards) { for (ZonedGrid zg : b.getAllDescendantComponentsOf(ZonedGrid.class)) { Zone z = zg.findZone(name); if (z != null) { return z; } } } return null; } /** * Search on all boards for a Region with the given name * @param Region name * @return Located region */ public Region findRegion(String name) { for (Board b : boards) { for (RegionGrid rg : b.getAllDescendantComponentsOf(RegionGrid.class)) { Region r = rg.findRegion(name); if (r != null) { return r; } } } return null; } /** * Return the board with the given name * * @param name * @return null if no such board found */ public Board getBoardByName(String name) { if (name != null) { for (Board b : boards) { if (name.equals(b.getName())) { return b; } } } return null; } public Dimension getPreferredSize() { final Dimension size = mapSize(); size.width *= getZoom(); size.height *= getZoom(); return size; } /** * @return the size of the map in pixels at 100% zoom, * including the edge buffer */ // FIXME: why synchronized? public synchronized Dimension mapSize() { final Rectangle r = new Rectangle(0,0); for (Board b : boards) r.add(b.bounds()); r.width += edgeBuffer.width; r.height += edgeBuffer.height; return r.getSize(); } /** * @return true if the given point may not be a legal location. I.e., if this grid will attempt to snap it to the * nearest grid location */ public boolean isLocationRestricted(Point p) { Board b = findBoard(p); if (b != null) { Rectangle r = b.bounds(); Point snap = new Point(p); snap.translate(-r.x, -r.y); return b.isLocationRestricted(snap); } else { return false; } } /** * @return the nearest allowable point according to the {@link VASSAL.build.module.map.boardPicker.board.MapGrid} on * the {@link Board} at this point * * @see Board#snapTo * @see VASSAL.build.module.map.boardPicker.board.MapGrid#snapTo */ public Point snapTo(Point p) { Point snap = new Point(p); final Board b = findBoard(p); if (b == null) return snap; final Rectangle r = b.bounds(); snap.translate(-r.x, -r.y); snap = b.snapTo(snap); snap.translate(r.x, r.y); // RFE 882378 // If we have snapped to a point 1 pixel off the edge of the map, move // back // onto the map. if (findBoard(snap) == null) { snap.translate(-r.x, -r.y); if (snap.x == r.width) { snap.x = r.width - 1; } else if (snap.x == -1) { snap.x = 0; } if (snap.y == r.height) { snap.y = r.height - 1; } else if (snap.y == -1) { snap.y = 0; } snap.translate(r.x, r.y); } return snap; } /** * The buffer of empty space around the boards in the Map window, * in component coordinates at 100% zoom */ public Dimension getEdgeBuffer() { return new Dimension(edgeBuffer); } /** * Translate a point from component coordinates (i.e., x,y position on * the JPanel) to map coordinates (i.e., accounting for zoom factor). * * @see #componentCoordinates */ public Point mapCoordinates(Point p) { p = new Point(p); p.x /= getZoom(); p.y /= getZoom(); return p; } public Rectangle mapRectangle(Rectangle r) { r = new Rectangle(r); r.x /= getZoom(); r.y /= getZoom(); r.width /= getZoom(); r.height /= getZoom(); return r; } /** * Translate a point from map coordinates to component coordinates * * @see #mapCoordinates */ public Point componentCoordinates(Point p) { p = new Point(p); p.x *= getZoom(); p.y *= getZoom(); return p; } public Rectangle componentRectangle(Rectangle r) { r = new Rectangle(r); r.x *= getZoom(); r.y *= getZoom(); r.width *= getZoom(); r.height *= getZoom(); return r; } /** * @return a String name for the given location on the map * * @see Board#locationName */ public String locationName(Point p) { String loc = getDeckNameAt(p); if (loc == null) { Board b = findBoard(p); if (b != null) { loc = b.locationName(new Point(p.x - b.bounds().x, p.y - b.bounds().y)); } } if (loc == null) { loc = Resources.getString("Map.offboard"); //$NON-NLS-1$ } return loc; } public String localizedLocationName(Point p) { String loc = getLocalizedDeckNameAt(p); if (loc == null) { Board b = findBoard(p); if (b != null) { loc = b.localizedLocationName(new Point(p.x - b.bounds().x, p.y - b.bounds().y)); } } if (loc == null) { loc = Resources.getString("Map.offboard"); //$NON-NLS-1$ } return loc; } /** * @return a String name for the given location on the map. Include Map name if requested. Report deck name instead of * location if point is inside the bounds of a deck. Do not include location if this map is not visible to all * players. */ // public String getFullLocationName(Point p, boolean includeMap) { // String loc = ""; //$NON-NLS-1$ // if (includeMap && getMapName() != null && getMapName().length() > 0) { // loc = "[" + getMapName() + "]"; //$NON-NLS-1$ //$NON-NLS-2$ // } // if (isVisibleToAll() && p != null) { // String pos = getDeckNameContaining(p); // if (pos == null) { // if (locationName(p) != null) { // loc = locationName(p) + loc; // } // } // else { // loc = pos; // } // } // return loc; // } /** * Is this map visible to all players */ public boolean isVisibleToAll() { if (this instanceof PrivateMap) { if (!getAttributeValueString(PrivateMap.VISIBLE).equals("true")) { //$NON-NLS-1$ return false; } } return true; } /** * Return the name of the deck whose bounding box contains p */ public String getDeckNameContaining(Point p) { String deck = null; if (p != null) { for (DrawPile d : getComponentsOf(DrawPile.class)) { Rectangle box = d.boundingBox(); if (box != null && box.contains(p)) { deck = d.getConfigureName(); break; } } } return deck; } /** * Return the name of the deck whose position is p * * @param p * @return */ public String getDeckNameAt(Point p) { String deck = null; if (p != null) { for (DrawPile d : getComponentsOf(DrawPile.class)) { if (d.getPosition().equals(p)) { deck = d.getConfigureName(); break; } } } return deck; } public String getLocalizedDeckNameAt(Point p) { String deck = null; if (p != null) { for (DrawPile d : getComponentsOf(DrawPile.class)) { if (d.getPosition().equals(p)) { deck = d.getLocalizedConfigureName(); break; } } } return deck; } /** * Because MouseEvents are received in component coordinates, it is inconvenient for MouseListeners on the map to have * to translate to map coordinates. MouseListeners added with this method will receive mouse events with points * already translated into map coordinates. * addLocalMouseListenerFirst inserts the new listener at the start of the chain. */ public void addLocalMouseListener(MouseListener l) { multicaster = AWTEventMulticaster.add(multicaster, l); } public void addLocalMouseListenerFirst(MouseListener l) { multicaster = AWTEventMulticaster.add(l, multicaster); } public void removeLocalMouseListener(MouseListener l) { multicaster = AWTEventMulticaster.remove(multicaster, l); } /** * MouseListeners on a map may be pushed and popped onto a stack. * Only the top listener on the stack receives mouse events. */ public void pushMouseListener(MouseListener l) { mouseListenerStack.add(l); } /** * MouseListeners on a map may be pushed and popped onto a stack. Only the top listener on the stack receives mouse * events */ public void popMouseListener() { mouseListenerStack.remove(mouseListenerStack.size()-1); } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } /** * Mouse events are first translated into map coordinates. Then the event is forwarded to the top MouseListener in the * stack, if any, otherwise forwarded to all LocalMouseListeners * * @see #pushMouseListener * @see #popMouseListener * @see #addLocalMouseListener */ public void mouseClicked(MouseEvent e) { if (!mouseListenerStack.isEmpty()) { final Point p = mapCoordinates(e.getPoint()); e.translatePoint(p.x - e.getX(), p.y - e.getY()); mouseListenerStack.get(mouseListenerStack.size()-1).mouseClicked(e); } else if (multicaster != null) { final Point p = mapCoordinates(e.getPoint()); e.translatePoint(p.x - e.getX(), p.y - e.getY()); multicaster.mouseClicked(e); } } /** * Mouse events are first translated into map coordinates. Then the event is forwarded to the top MouseListener in the * stack, if any, otherwise forwarded to all LocalMouseListeners * * @see #pushMouseListener * @see #popMouseListener * @see #addLocalMouseListener */ public static Map activeMap = null; public static void clearActiveMap() { if (activeMap != null) { activeMap.repaint(); activeMap = null; } } public void mousePressed(MouseEvent e) { // Deselect any counters on the last Map with focus if (!this.equals(activeMap)) { boolean dirty = false; final ArrayList<GamePiece> l = new ArrayList<GamePiece>(); for (Iterator<GamePiece> i = KeyBuffer.getBuffer().getPiecesIterator(); i.hasNext(); ) { l.add(i.next()); } for (GamePiece p : l) { if (p.getMap() == activeMap) { KeyBuffer.getBuffer().remove(p); dirty = true; } } if (dirty && activeMap != null) { activeMap.repaint(); } } activeMap = this; if (!mouseListenerStack.isEmpty()) { final Point p = mapCoordinates(e.getPoint()); e.translatePoint(p.x - e.getX(), p.y - e.getY()); mouseListenerStack.get(mouseListenerStack.size()-1).mousePressed(e); } else if (multicaster != null) { final Point p = mapCoordinates(e.getPoint()); e.translatePoint(p.x - e.getX(), p.y - e.getY()); multicaster.mousePressed(e); } } /** * Mouse events are first translated into map coordinates. * Then the event is forwarded to the top MouseListener in the * stack, if any, otherwise forwarded to all LocalMouseListeners. * * @see #pushMouseListener * @see #popMouseListener * @see #addLocalMouseListener */ public void mouseReleased(MouseEvent e) { Point p = e.getPoint(); p.translate(theMap.getX(), theMap.getY()); if (theMap.getBounds().contains(p)) { if (!mouseListenerStack.isEmpty()) { p = mapCoordinates(e.getPoint()); e.translatePoint(p.x - e.getX(), p.y - e.getY()); mouseListenerStack.get(mouseListenerStack.size()-1).mouseReleased(e); } else if (multicaster != null) { p = mapCoordinates(e.getPoint()); e.translatePoint(p.x - e.getX(), p.y - e.getY()); multicaster.mouseReleased(e); } // Request Focus so that keyboard input will be recognized theMap.requestFocus(); } // Clicking with mouse always repaints the map clearFirst = true; theMap.repaint(); activeMap = this; } /** * Save all current Key Listeners and remove them from the * map. Used by Traits that need to prevent Key Commands * at certain times. */ public void enableKeyListeners() { if (saveKeyListeners == null) return; for (KeyListener kl : saveKeyListeners) { theMap.addKeyListener(kl); } saveKeyListeners = null; } /** * Restore the previously disabled KeyListeners */ public void disableKeyListeners() { if (saveKeyListeners != null) return; saveKeyListeners = theMap.getKeyListeners(); for (KeyListener kl : saveKeyListeners) { theMap.removeKeyListener(kl); } } /** * This listener will be notified when a drag event is initiated, assuming that no MouseListeners are on the stack. * * @see #pushMouseListener * @param dragGestureListener */ public void setDragGestureListener(DragGestureListener dragGestureListener) { this.dragGestureListener = dragGestureListener; } public DragGestureListener getDragGestureListener() { return dragGestureListener; } public void dragEnter(DropTargetDragEvent dtde) { } public void dragOver(DropTargetDragEvent dtde) { scrollAtEdge(dtde.getLocation(), SCROLL_ZONE); } public void dropActionChanged(DropTargetDragEvent dtde) { } /* * Cancel final scroll and repaint map */ public void dragExit(DropTargetEvent dte) { if (scroller.isRunning()) scroller.stop(); repaint(); } public void drop(DropTargetDropEvent dtde) { if (dtde.getDropTargetContext().getComponent() == theMap) { final MouseEvent evt = new MouseEvent( theMap, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(), 0, dtde.getLocation().x, dtde.getLocation().y, 1, false ); theMap.dispatchEvent(evt); dtde.dropComplete(true); } if (scroller.isRunning()) scroller.stop(); } /** * Mouse motion events are not forwarded to LocalMouseListeners or to listeners on the stack */ public void mouseMoved(MouseEvent e) { } /** * Mouse motion events are not forwarded to LocalMouseListeners or to * listeners on the stack. * * The map scrolls when dragging the mouse near the edge. */ public void mouseDragged(MouseEvent e) { if (!e.isMetaDown()) { scrollAtEdge(e.getPoint(), SCROLL_ZONE); } else { if (scroller.isRunning()) scroller.stop(); } } /* * Delay before starting scroll at edge */ public static final int PREFERRED_EDGE_SCROLL_DELAY = 200; public static final String PREFERRED_EDGE_DELAY = "PreferredEdgeDelay"; //$NON-NLS-1$ /** The width of the hot zone for triggering autoscrolling. */ public static final int SCROLL_ZONE = 30; /** The horizontal component of the autoscrolling vector, -1, 0, or 1. */ protected int sx; /** The vertical component of the autoscrolling vector, -1, 0, or 1. */ protected int sy; protected int dx, dy; /** * Begin autoscrolling the map if the given point is within the given * distance from a viewport edge. * * @param evtPt * @param dist */ public void scrollAtEdge(Point evtPt, int dist) { final Rectangle vrect = scroll.getViewport().getViewRect(); final int px = evtPt.x - vrect.x; final int py = evtPt.y - vrect.y; // determine scroll vector sx = 0; if (px < dist && px >= 0) { sx = -1; dx = dist - px; } else if (px < vrect.width && px >= vrect.width - dist) { sx = 1; dx = dist - (vrect.width - px); } sy = 0; if (py < dist && py >= 0) { sy = -1; dy = dist - py; } else if (py < vrect.height && py >= vrect.height - dist) { sy = 1; dy = dist - (vrect.height - py); } dx /= 2; dy /= 2; // start autoscrolling if we have a nonzero scroll vector if (sx != 0 || sy != 0) { if (!scroller.isRunning()) { scroller.setStartDelay((Integer) GameModule.getGameModule().getPrefs().getValue(PREFERRED_EDGE_DELAY)); scroller.start(); } } else { if (scroller.isRunning()) scroller.stop(); } } /** The animator which controls autoscrolling. */ protected Animator scroller = new Animator(Animator.INFINITE, new TimingTargetAdapter() { private long t0; @Override public void timingEvent(float fraction) { // Constant velocity along each axis, 0.5px/ms final long t1 = System.currentTimeMillis(); final int dt = (int)((t1 - t0)/2); t0 = t1; scroll(sx*dt, sy*dt); // Check whether we have hit an edge final Rectangle vrect = scroll.getViewport().getViewRect(); if ((sx == -1 && vrect.x == 0) || (sx == 1 && vrect.x + vrect.width >= theMap.getWidth())) sx = 0; if ((sy == -1 && vrect.y == 0) || (sy == 1 && vrect.y + vrect.height >= theMap.getHeight())) sy = 0; // Stop if the scroll vector is zero if (sx == 0 && sy == 0) scroller.stop(); } @Override public void begin() { t0 = System.currentTimeMillis(); } } ); public void repaint(boolean cf) { clearFirst = cf; theMap.repaint(); } /** * Painting the map is done in three steps: 1) draw each of the {@link Board}s on the map. 2) draw all of the * counters on the map. 3) draw all of the {@link Drawable} components on the map * * @see #addDrawComponent * @see #setBoards * @see #addPiece */ public void paint(Graphics g) { paint(g, 0, 0); } public void paintRegion(Graphics g, Rectangle visibleRect) { paintRegion(g, visibleRect, theMap); } public void paintRegion(Graphics g, Rectangle visibleRect, Component c) { clearMapBorder(g); // To avoid ghost pieces around the edge drawBoardsInRegion(g, visibleRect, c); drawDrawable(g, false); drawPiecesInRegion(g, visibleRect, c); drawDrawable(g, true); } public void drawBoardsInRegion(Graphics g, Rectangle visibleRect, Component c) { for (Board b : boards) { b.drawRegion(g, getLocation(b, getZoom()), visibleRect, getZoom(), c); } } public void drawBoardsInRegion(Graphics g, Rectangle visibleRect) { drawBoardsInRegion(g, visibleRect, theMap); } public void repaint() { theMap.repaint(); } public void drawPiecesInRegion(Graphics g, Rectangle visibleRect, Component c) { if (!hideCounters) { Graphics2D g2d = (Graphics2D) g; Composite oldComposite = g2d.getComposite(); g2d.setComposite( AlphaComposite.getInstance(AlphaComposite.SRC_OVER, pieceOpacity)); GamePiece[] stack = pieces.getPieces(); for (int i = 0; i < stack.length; ++i) { Point pt = componentCoordinates(stack[i].getPosition()); if (stack[i].getClass() == Stack.class) { getStackMetrics().draw( (Stack) stack[i], pt, g, this, getZoom(), visibleRect); } else { stack[i].draw(g, pt.x, pt.y, c, getZoom()); if (Boolean.TRUE.equals(stack[i].getProperty(Properties.SELECTED))) { highlighter.draw(stack[i], g, pt.x, pt.y, c, getZoom()); } } /* // draw bounding box for debugging final Rectangle bb = stack[i].boundingBox(); g.drawRect(pt.x + bb.x, pt.y + bb.y, bb.width, bb.height); */ } g2d.setComposite(oldComposite); } } public void drawPiecesInRegion(Graphics g, Rectangle visibleRect) { drawPiecesInRegion(g, visibleRect, theMap); } public void drawPieces(Graphics g, int xOffset, int yOffset) { if (!hideCounters) { Graphics2D g2d = (Graphics2D) g; Composite oldComposite = g2d.getComposite(); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, pieceOpacity)); GamePiece[] stack = pieces.getPieces(); for (int i = 0; i < stack.length; ++i) { Point pt = componentCoordinates(stack[i].getPosition()); stack[i].draw(g, pt.x + xOffset, pt.y + yOffset, theMap, getZoom()); if (Boolean.TRUE.equals(stack[i].getProperty(Properties.SELECTED))) { highlighter.draw(stack[i], g, pt.x - xOffset, pt.y - yOffset, theMap, getZoom()); } } g2d.setComposite(oldComposite); } } public void drawDrawable(Graphics g, boolean aboveCounters) { for (Drawable drawable : drawComponents) { if (!(aboveCounters ^ drawable.drawAboveCounters())) { drawable.draw(g, this); } } } /** * Paint the map at the given offset, i.e. such that (xOffset, yOffset) is in the upper left corner */ public void paint(Graphics g, int xOffset, int yOffset) { drawBoards(g, xOffset, yOffset, getZoom(), theMap); drawDrawable(g, false); drawPieces(g, xOffset, yOffset); drawDrawable(g, true); } public Highlighter getHighlighter() { return highlighter; } public void setHighlighter(Highlighter h) { highlighter = h; } public void addHighlighter(Highlighter h) { highlighters.add(h); } public void removeHighlighter(Highlighter h) { highlighters.remove(h); } public Iterator<Highlighter> getHighlighters() { return highlighters.iterator(); } /** * @return a Collection of all {@link Board}s on the Map */ public Collection<Board> getBoards() { return Collections.unmodifiableCollection(boards); } /** * @return an Enumeration of all {@link Board}s on the map * @deprecated Use {@link #getBoards()} instead. */ @Deprecated public Enumeration<Board> getAllBoards() { return Collections.enumeration(boards); } public int getBoardCount() { return boards.size(); } /** * Returns the boundingBox of a GamePiece accounting for the offset of a piece within its parent stack. Return null if * this piece is not on the map * * @see GamePiece#boundingBox */ public Rectangle boundingBoxOf(GamePiece p) { Rectangle r = null; if (p.getMap() == this) { r = p.boundingBox(); final Point pos = p.getPosition(); r.translate(pos.x, pos.y); if (Boolean.TRUE.equals(p.getProperty(Properties.SELECTED))) { r.add(highlighter.boundingBox(p)); for (Iterator<Highlighter> i = getHighlighters(); i.hasNext();) { r.add(i.next().boundingBox(p)); } } if (p.getParent() != null) { final Point pt = getStackMetrics().relativePosition(p.getParent(), p); r.translate(pt.x, pt.y); } } return r; } /** * Returns the selection bounding box of a GamePiece accounting for the offset of a piece within a stack * * @see GamePiece#getShape */ public Rectangle selectionBoundsOf(GamePiece p) { if (p.getMap() != this) { throw new IllegalArgumentException( Resources.getString("Map.piece_not_on_map")); //$NON-NLS-1$ } final Rectangle r = p.getShape().getBounds(); r.translate(p.getPosition().x, p.getPosition().y); if (p.getParent() != null) { Point pt = getStackMetrics().relativePosition(p.getParent(), p); r.translate(pt.x, pt.y); } return r; } /** * Returns the position of a GamePiece accounting for the offset within a parent stack, if any */ public Point positionOf(GamePiece p) { if (p.getMap() != this) { throw new IllegalArgumentException( Resources.getString("Map.piece_not_on_map")); //$NON-NLS-1$ } final Point point = p.getPosition(); if (p.getParent() != null) { final Point pt = getStackMetrics().relativePosition(p.getParent(), p); point.translate(pt.x, pt.y); } return point; } /** * @return an array of all GamePieces on the map. This is a read-only copy. * Altering the array does not alter the pieces on the map. */ public GamePiece[] getPieces() { return pieces.getPieces(); } public GamePiece[] getAllPieces() { return pieces.getAllPieces(); } public void setPieceCollection(PieceCollection pieces) { this.pieces = pieces; } public PieceCollection getPieceCollection() { return pieces; } protected void clearMapBorder(Graphics g) { if (clearFirst || boards.isEmpty()) { g.setColor(bgColor); g.fillRect(0, 0, theMap.getWidth(), theMap.getHeight()); clearFirst = false; } else { final Dimension buffer = new Dimension( (int) (getZoom() * edgeBuffer.width), (int) (getZoom() * edgeBuffer.height)); g.setColor(bgColor); g.fillRect(0, 0, buffer.width, theMap.getHeight()); g.fillRect(0, 0, theMap.getWidth(), buffer.height); g.fillRect(theMap.getWidth() - buffer.width, 0, buffer.width, theMap.getHeight()); g.fillRect(0, theMap.getHeight() - buffer.height, theMap.getWidth(), buffer.height); } } /** * Adjusts the bounds() rectangle to account for the Board's relative * position to other boards. In other words, if Board A is N pixels wide * and Board B is to the right of Board A, then the origin of Board B * will be adjusted N pixels to the right. */ protected void setBoardBoundaries() { int maxX = 0; int maxY = 0; for (Board b : boards) { Point relPos = b.relativePosition(); maxX = Math.max(maxX, relPos.x); maxY = Math.max(maxY, relPos.y); } boardWidths = new int[maxX + 1][maxY + 1]; boardHeights = new int[maxX + 1][maxY + 1]; for (Board b : boards) { Point relPos = b.relativePosition(); boardWidths[relPos.x][relPos.y] = b.bounds().width; boardHeights[relPos.x][relPos.y] = b.bounds().height; } Point offset = new Point(edgeBuffer.width, edgeBuffer.height); for (Board b : boards) { Point relPos = b.relativePosition(); Point location = getLocation(relPos.x, relPos.y, 1.0); b.setLocation(location.x, location.y); b.translate(offset.x, offset.y); } theMap.revalidate(); } protected Point getLocation(Board b, double zoom) { Point p; if (zoom == 1.0) { p = b.bounds().getLocation(); } else { Point relPos = b.relativePosition(); p = getLocation(relPos.x, relPos.y, zoom); p.translate((int) (zoom * edgeBuffer.width), (int) (zoom * edgeBuffer.height)); } return p; } protected Point getLocation(int column, int row, double zoom) { Point p = new Point(); for (int x = 0; x < column; ++x) { p.translate((int) Math.floor(zoom * boardWidths[x][row]), 0); } for (int y = 0; y < row; ++y) { p.translate(0, (int) Math.floor(zoom * boardHeights[column][y])); } return p; } /** * Draw the boards of the map at the given point and zoom factor onto * the given Graphics object */ public void drawBoards(Graphics g, int xoffset, int yoffset, double zoom, Component obs) { for (Board b : boards) { Point p = getLocation(b, zoom); p.translate(xoffset, yoffset); b.draw(g, p.x, p.y, zoom, obs); } } /** * Repaint the given area, specified in map coordinates */ public void repaint(Rectangle r) { r.setLocation(componentCoordinates(new Point(r.x, r.y))); r.setSize((int) (r.width * getZoom()), (int) (r.height * getZoom())); theMap.repaint(r.x, r.y, r.width, r.height); } /** * @param show * if true, enable drawing of GamePiece. If false, don't draw GamePiece when painting the map */ public void setPiecesVisible(boolean show) { hideCounters = !show; } public boolean isPiecesVisible() { return !hideCounters && pieceOpacity != 0; } public float getPieceOpacity() { return pieceOpacity; } public void setPieceOpacity(float pieceOpacity) { this.pieceOpacity = pieceOpacity; } public Object getProperty(Object key) { Object value = null; MutableProperty p = propsContainer.getMutableProperty(String.valueOf(key)); if (p != null) { value = p.getPropertyValue(); } else { value = GameModule.getGameModule().getProperty(key); } return value; } public Object getLocalizedProperty(Object key) { Object value = null; MutableProperty p = propsContainer.getMutableProperty(String.valueOf(key)); if (p != null) { value = p.getPropertyValue(); } if (value == null) { value = GameModule.getGameModule().getLocalizedProperty(key); } return value; } /** * Return the auto-move key. It may be named, so just return * the allocated KeyStroke. * @return auto move keystroke */ public KeyStroke getMoveKey() { return moveKey == null ? null : moveKey.getKeyStroke(); } /** * @return the top-level window containing this map */ protected Window createParentFrame() { if (GlobalOptions.getInstance().isUseSingleWindow()) { JDialog d = new JDialog(GameModule.getGameModule().getFrame()); d.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); d.setTitle(getDefaultWindowTitle()); return d; } else { JFrame d = new JFrame(); d.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); d.setTitle(getDefaultWindowTitle()); d.setJMenuBar(MenuManager.getInstance().getMenuBarFor(d)); return d; } } public boolean shouldDockIntoMainWindow() { boolean shouldDock = false; if (GlobalOptions.getInstance().isUseSingleWindow() && !useLaunchButton) { shouldDock = true; for (Map m : GameModule.getGameModule().getComponentsOf(Map.class)) { if (m == this) { break; } if (m.shouldDockIntoMainWindow()) { shouldDock = false; break; } } } return shouldDock; } /** * When a game is started, create a top-level window, if none exists. * When a game is ended, remove all boards from the map. * * @see GameComponent */ public void setup(boolean show) { if (show) { final GameModule g = GameModule.getGameModule(); if (shouldDockIntoMainWindow()) { mainWindowDock.showComponent(); final int height = ((Integer) Prefs.getGlobalPrefs().getValue(MAIN_WINDOW_HEIGHT)).intValue(); if (height > 0) { final Container top = mainWindowDock.getTopLevelAncestor(); top.setSize(top.getWidth(), height); } if (toolBar.getParent() == null) { g.getToolBar().addSeparator(); g.getToolBar().add(toolBar); } toolBar.setVisible(true); } else { if (SwingUtilities.getWindowAncestor(theMap) == null) { final Window topWindow = createParentFrame(); topWindow.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { if (useLaunchButton) { topWindow.setVisible(false); } else { g.getGameState().setup(false); } } }); ((RootPaneContainer) topWindow).getContentPane().add("North", getToolBar()); //$NON-NLS-1$ ((RootPaneContainer) topWindow).getContentPane().add("Center", layeredPane); //$NON-NLS-1$ topWindow.setSize(600, 400); final PositionOption option = new PositionOption(PositionOption.key + getIdentifier(), topWindow); g.getPrefs().addOption(option); } theMap.getTopLevelAncestor().setVisible(!useLaunchButton); theMap.revalidate(); } } else { pieces.clear(); boards.clear(); if (mainWindowDock != null) { if (mainWindowDock.getHideableComponent().isShowing()) { Prefs.getGlobalPrefs().getOption(MAIN_WINDOW_HEIGHT) .setValue(mainWindowDock.getTopLevelAncestor().getHeight()); } mainWindowDock.hideComponent(); toolBar.setVisible(false); } else if (theMap.getTopLevelAncestor() != null) { theMap.getTopLevelAncestor().setVisible(false); } } launchButton.setEnabled(show); launchButton.setVisible(useLaunchButton); } public void appendToTitle(String s) { if (mainWindowDock == null) { Component c = theMap.getTopLevelAncestor(); if (s == null) { if (c instanceof JFrame) { ((JFrame) c).setTitle(getDefaultWindowTitle()); } if (c instanceof JDialog) { ((JDialog) c).setTitle(getDefaultWindowTitle()); } } else { if (c instanceof JFrame) { ((JFrame) c).setTitle(((JFrame) c).getTitle() + s); } if (c instanceof JDialog) { ((JDialog) c).setTitle(((JDialog) c).getTitle() + s); } } } } protected String getDefaultWindowTitle() { return getLocalizedMapName().length() > 0 ? getLocalizedMapName() : Resources.getString("Map.window_title", GameModule.getGameModule().getLocalizedGameName()); //$NON-NLS-1$ } /** * Use the provided {@link PieceFinder} instance to locate a visible piece at the given location */ public GamePiece findPiece(Point pt, PieceFinder finder) { GamePiece[] stack = pieces.getPieces(); for (int i = stack.length - 1; i >= 0; --i) { GamePiece p = finder.select(this, stack[i], pt); if (p != null) { return p; } } return null; } /** * Use the provided {@link PieceFinder} instance to locate any piece at the given location, regardless of whether it * is visible or not */ public GamePiece findAnyPiece(Point pt, PieceFinder finder) { GamePiece[] stack = pieces.getAllPieces(); for (int i = stack.length - 1; i >= 0; --i) { GamePiece p = finder.select(this, stack[i], pt); if (p != null) { return p; } } return null; } /** * Place a piece at the destination point. If necessary, remove the piece from its parent Stack or Map * * @return a {@link Command} that reproduces this action */ public Command placeAt(GamePiece piece, Point pt) { Command c = null; if (GameModule.getGameModule().getGameState().getPieceForId(piece.getId()) == null) { piece.setPosition(pt); addPiece(piece); GameModule.getGameModule().getGameState().addPiece(piece); c = new AddPiece(piece); } else { MoveTracker tracker = new MoveTracker(piece); piece.setPosition(pt); addPiece(piece); c = tracker.getMoveCommand(); } return c; } /** * Apply the provided {@link PieceVisitorDispatcher} to all pieces on this map. Returns the first non-null * {@link Command} returned by <code>commandFactory</code> * * @param commandFactory * */ public Command apply(PieceVisitorDispatcher commandFactory) { GamePiece[] stack = pieces.getPieces(); Command c = null; for (int i = 0; i < stack.length && c == null; ++i) { c = (Command) commandFactory.accept(stack[i]); } return c; } /** * Move a piece to the destination point. If a piece is at the point (i.e. has a location exactly equal to it), merge * with the piece by forwarding to {@link StackMetrics#merge}. Otherwise, place by forwarding to placeAt() * * @see StackMetrics#merge */ public Command placeOrMerge(final GamePiece p, final Point pt) { Command c = apply(new DeckVisitorDispatcher(new Merger(this, pt, p))); if (c == null || c.isNull()) { c = placeAt(p, pt); // If no piece at destination and this is a stacking piece, create // a new Stack containing the piece if (!(p instanceof Stack) && !Boolean.TRUE.equals(p.getProperty(Properties.NO_STACK))) { final Stack parent = getStackMetrics().createStack(p); if (parent != null) { c = c.append(placeAt(parent, pt)); } } } return c; } /** * Adds a GamePiece to this map. Removes the piece from its parent Stack and from its current map, if different from * this map */ public void addPiece(GamePiece p) { if (indexOf(p) < 0) { if (p.getParent() != null) { p.getParent().remove(p); p.setParent(null); } if (p.getMap() != null && p.getMap() != this) { p.getMap().removePiece(p); } pieces.add(p); p.setMap(this); theMap.repaint(); } } /** * Reorder the argument GamePiece to the new index. When painting the map, pieces are drawn in order of index * * @deprecated use {@link PieceCollection#moveToFront} */ @Deprecated public void reposition(GamePiece s, int pos) { } /** * Returns the index of a piece. When painting the map, pieces are drawn in order of index Return -1 if the piece is * not on this map */ public int indexOf(GamePiece s) { return pieces.indexOf(s); } /** * Removes a piece from the map */ public void removePiece(GamePiece p) { pieces.remove(p); theMap.repaint(); } /** * Center the map at given map coordinates within its JScrollPane container */ public void centerAt(Point p) { centerAt(p, 0, 0); } /** * Center the map at the given map coordinates, if the point is not * already within (dx,dy) of the center. */ public void centerAt(Point p, int dx, int dy) { if (scroll != null) { p = componentCoordinates(p); final Rectangle r = theMap.getVisibleRect(); r.x = p.x - r.width/2; r.y = p.y - r.height/2; final Dimension d = getPreferredSize(); if (r.x + r.width > d.width) r.x = d.width - r.width; if (r.y + r.height > d.height) r.y = d.height - r.height; r.width = dx > r.width ? 0 : r.width - dx; r.height = dy > r.height ? 0 : r.height - dy; theMap.scrollRectToVisible(r); } } /** Ensure that the given region (in map coordinates) is visible */ public void ensureVisible(Rectangle r) { if (scroll != null) { final Point p = componentCoordinates(r.getLocation()); r = new Rectangle(p.x, p.y, (int) (getZoom() * r.width), (int) (getZoom() * r.height)); theMap.scrollRectToVisible(r); } } /** * Scrolls the map in the containing JScrollPane. * * @param dx number of pixels to scroll horizontally * @param dy number of pixels to scroll vertically */ public void scroll(int dx, int dy) { Rectangle r = scroll.getViewport().getViewRect(); r.translate(dx, dy); r = r.intersection(new Rectangle(getPreferredSize())); theMap.scrollRectToVisible(r); } public static String getConfigureTypeName() { return Resources.getString("Editor.Map.component_type"); //$NON-NLS-1$ } public String getMapName() { return getConfigureName(); } public String getLocalizedMapName() { return getLocalizedConfigureName(); } public void setMapName(String s) { mapName = s; setConfigureName(mapName); if (tooltip == null || tooltip.length() == 0) { launchButton.setToolTipText(s != null ? Resources.getString("Map.show_hide", s) : Resources.getString("Map.show_hide", Resources.getString("Map.map"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } public HelpFile getHelpFile() { return HelpFile.getReferenceManualPage("Map.htm"); //$NON-NLS-1$ } public String[] getAttributeDescriptions() { return new String[] { Resources.getString("Editor.Map.map_name"), //$NON-NLS-1$ Resources.getString("Editor.Map.mark_pieces_moved"), //$NON-NLS-1$ Resources.getString("Editor.Map.mark_unmoved_button_text"), //$NON-NLS-1$ Resources.getString("Editor.Map.mark_unmoved_tooltip_text"), //$NON-NLS-1$ Resources.getString("Editor.Map.mark_unmoved_button_icon"), //$NON-NLS-1$ Resources.getString("Editor.Map.horizontal"), //$NON-NLS-1$ Resources.getString("Editor.Map.vertical"), //$NON-NLS-1$ Resources.getString("Editor.Map.bkgdcolor"), //$NON-NLS-1$ Resources.getString("Editor.Map.multiboard"), //$NON-NLS-1$ Resources.getString("Editor.Map.bc_selected_counter"), //$NON-NLS-1$ Resources.getString("Editor.Map.bt_selected_counter"), //$NON-NLS-1$ Resources.getString("Editor.Map.show_hide"), //$NON-NLS-1$ Resources.getString(Resources.BUTTON_TEXT), Resources.getString(Resources.TOOLTIP_TEXT), Resources.getString(Resources.BUTTON_ICON), Resources.getString(Resources.HOTKEY_LABEL), Resources.getString("Editor.Map.report_move_within"), //$NON-NLS-1$ Resources.getString("Editor.Map.report_move_to"), //$NON-NLS-1$ Resources.getString("Editor.Map.report_created"), //$NON-NLS-1$ Resources.getString("Editor.Map.report_modified"), //$NON-NLS-1$ Resources.getString("Editor.Map.key_applied_all") //$NON-NLS-1$ }; } public String[] getAttributeNames() { return new String[] { NAME, MARK_MOVED, MARK_UNMOVED_TEXT, MARK_UNMOVED_TOOLTIP, MARK_UNMOVED_ICON, EDGE_WIDTH, EDGE_HEIGHT, BACKGROUND_COLOR, ALLOW_MULTIPLE, HIGHLIGHT_COLOR, HIGHLIGHT_THICKNESS, USE_LAUNCH_BUTTON, BUTTON_NAME, TOOLTIP, ICON, HOTKEY, MOVE_WITHIN_FORMAT, MOVE_TO_FORMAT, CREATE_FORMAT, CHANGE_FORMAT, MOVE_KEY }; } public Class<?>[] getAttributeTypes() { return new Class<?>[] { String.class, GlobalOptions.Prompt.class, String.class, String.class, UnmovedIconConfig.class, Integer.class, Integer.class, Color.class, Boolean.class, Color.class, Integer.class, Boolean.class, String.class, String.class, IconConfig.class, NamedKeyStroke.class, MoveWithinFormatConfig.class, MoveToFormatConfig.class, CreateFormatConfig.class, ChangeFormatConfig.class, NamedKeyStroke.class }; } public static final String LOCATION = "location"; //$NON-NLS-1$ public static final String OLD_LOCATION = "previousLocation"; //$NON-NLS-1$ public static final String OLD_MAP = "previousMap"; //$NON-NLS-1$ public static final String MAP_NAME = "mapName"; //$NON-NLS-1$ public static final String PIECE_NAME = "pieceName"; //$NON-NLS-1$ public static final String MESSAGE = "message"; //$NON-NLS-1$ public static class IconConfig implements ConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new IconConfigurer(key, name, "/images/map.gif"); //$NON-NLS-1$ } } public static class UnmovedIconConfig implements ConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new IconConfigurer(key, name, "/images/unmoved.gif"); //$NON-NLS-1$ } } public static class MoveWithinFormatConfig implements TranslatableConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new PlayerIdFormattedStringConfigurer(key, name, new String[] { PIECE_NAME, LOCATION, MAP_NAME, OLD_LOCATION }); } } public static class MoveToFormatConfig implements TranslatableConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new PlayerIdFormattedStringConfigurer(key, name, new String[] { PIECE_NAME, LOCATION, OLD_MAP, MAP_NAME, OLD_LOCATION }); } } public static class CreateFormatConfig implements TranslatableConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new PlayerIdFormattedStringConfigurer(key, name, new String[] { PIECE_NAME, MAP_NAME, LOCATION }); } } public static class ChangeFormatConfig implements TranslatableConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new PlayerIdFormattedStringConfigurer(key, name, new String[] { MESSAGE, ReportState.COMMAND_NAME, ReportState.OLD_UNIT_NAME, ReportState.NEW_UNIT_NAME, ReportState.MAP_NAME, ReportState.LOCATION_NAME }); } } public String getCreateFormat() { if (createFormat != null) { return createFormat; } else { String val = "$" + PIECE_NAME + "$ created in $" + LOCATION + "$"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ if (!boards.isEmpty()) { Board b = boards.get(0); if (b.getGrid() == null || b.getGrid().getGridNumbering() == null) { val = ""; //$NON-NLS-1$ } } return val; } } public String getChangeFormat() { return isChangeReportingEnabled() ? changeFormat : ""; } public String getMoveToFormat() { if (moveToFormat != null) { return moveToFormat; } else { String val = "$" + PIECE_NAME + "$" + " moves $" + OLD_LOCATION + "$ -> $" + LOCATION + "$ *"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ if (!boards.isEmpty()) { Board b = boards.get(0); if (b.getGrid() == null || b.getGrid().getGridNumbering() != null) { val = ""; //$NON-NLS-1$ } } return val; } } public String getMoveWithinFormat() { if (moveWithinFormat != null) { return moveWithinFormat; } else { String val = "$" + PIECE_NAME + "$" + " moves $" + OLD_LOCATION + "$ -> $" + LOCATION + "$ *"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ if (!boards.isEmpty()) { Board b = boards.get(0); if (b.getGrid() == null) { val = ""; //$NON-NLS-1$ } } return val; } } public Class<?>[] getAllowableConfigureComponents() { Class<?>[] c = { GlobalMap.class, LOS_Thread.class, ToolbarMenu.class, MultiActionButton.class, HidePiecesButton.class, Zoomer.class, CounterDetailViewer.class, HighlightLastMoved.class, LayeredPieceCollection.class, ImageSaver.class, TextSaver.class, DrawPile.class, SetupStack.class, MassKeyCommand.class, MapShader.class, PieceRecenterer.class }; return c; } public VisibilityCondition getAttributeVisibility(String name) { if (visibilityCondition == null) { visibilityCondition = new VisibilityCondition() { public boolean shouldBeVisible() { return useLaunchButton; } }; } if (HOTKEY.equals(name) || BUTTON_NAME.equals(name) || TOOLTIP.equals(name) || ICON.equals(name)) { return visibilityCondition; } else if (MARK_UNMOVED_TEXT.equals(name) || MARK_UNMOVED_ICON.equals(name) || MARK_UNMOVED_TOOLTIP.equals(name)) { return new VisibilityCondition() { public boolean shouldBeVisible() { return !GlobalOptions.NEVER.equals(markMovedOption); } }; } else { return super.getAttributeVisibility(name); } } /** * Each Map must have a unique String id */ public void setId(String id) { mapID = id; } public static Map getMapById(String id) { return (Map) idMgr.findInstance(id); } /** * Utility method to return a {@link List} of all map components in the * module. * * @return the list of <code>Map</code>s */ public static List<Map> getMapList() { final GameModule g = GameModule.getGameModule(); final List<Map> l = g.getComponentsOf(Map.class); for (ChartWindow cw : g.getComponentsOf(ChartWindow.class)) { for (MapWidget mw : cw.getAllDescendantComponentsOf(MapWidget.class)) { l.add(mw.getMap()); } } return l; } /** * Utility method to return a list of all map components in the module * * @return Iterator over all maps * @deprecated Use {@link #getMapList()} instead. */ @Deprecated public static Iterator<Map> getAllMaps() { return getMapList().iterator(); } /** * Find a contained Global Variable by name */ public MutableProperty getMutableProperty(String name) { return propsContainer.getMutableProperty(name); } public void addMutableProperty(String key, MutableProperty p) { propsContainer.addMutableProperty(key, p); p.addMutablePropertyChangeListener(repaintOnPropertyChange); } public MutableProperty removeMutableProperty(String key) { MutableProperty p = propsContainer.removeMutableProperty(key); if (p != null) { p.removeMutablePropertyChangeListener(repaintOnPropertyChange); } return p; } public String getMutablePropertiesContainerId() { return getMapName(); } /** * Each Map must have a unique String id * * @return the id for this map */ public String getId() { return mapID; } /** * Make a best gues for a unique identifier for the target. Use * {@link VASSAL.tools.UniqueIdManager.Identifyable#getConfigureName} if non-null, otherwise use * {@link VASSAL.tools.UniqueIdManager.Identifyable#getId} * * @return */ public String getIdentifier() { return UniqueIdManager.getIdentifier(this); } /** @return the Swing component representing the map */ public JComponent getView() { if (theMap == null) { theMap = new View(this); scroll = new AdjustableSpeedScrollPane( theMap, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); scroll.unregisterKeyboardAction( KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0)); scroll.unregisterKeyboardAction( KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0)); scroll.setAlignmentX(0.0f); scroll.setAlignmentY(0.0f); layeredPane.setLayout(new InsetLayout(layeredPane, scroll)); layeredPane.add(scroll, JLayeredPane.DEFAULT_LAYER); } return theMap; } /** @return the JLayeredPane holding map insets */ public JLayeredPane getLayeredPane() { return layeredPane; } /** * The Layout responsible for arranging insets which overlay the Map * InsetLayout currently is responsible for keeping the {@link GlobalMap} * in the upper-left corner of the {@link Map.View}. */ public static class InsetLayout extends OverlayLayout { private static final long serialVersionUID = 1L; private final JScrollPane base; public InsetLayout(Container target, JScrollPane base) { super(target); this.base = base; } public void layoutContainer(Container target) { super.layoutContainer(target); base.getLayout().layoutContainer(base); final Dimension viewSize = base.getViewport().getSize(); final Insets insets = base.getInsets(); viewSize.width += insets.left; viewSize.height += insets.top; // prevent non-base components from overlapping the base's scrollbars final int n = target.getComponentCount(); for (int i = 0; i < n; ++i) { Component c = target.getComponent(i); if (c != base && c.isVisible()) { final Rectangle b = c.getBounds(); b.width = Math.min(b.width, viewSize.width); b.height = Math.min(b.height, viewSize.height); c.setBounds(b); } } } } /** * Implements default logic for merging pieces at a given location within * a map Returns a {@link Command} that merges the input {@link GamePiece} * with an existing piece at the input position, provided the pieces are * stackable, visible, in the same layer, etc. */ public static class Merger implements DeckVisitor { private Point pt; private Map map; private GamePiece p; public Merger(Map map, Point pt, GamePiece p) { this.map = map; this.pt = pt; this.p = p; } public Object visitDeck(Deck d) { if (d.getPosition().equals(pt)) { return map.getStackMetrics().merge(d, p); } else { return null; } } public Object visitStack(Stack s) { if (s.getPosition().equals(pt) && map.getStackMetrics().isStackingEnabled() && !Boolean.TRUE.equals(p.getProperty(Properties.NO_STACK)) && s.topPiece() != null && map.getPieceCollection().canMerge(s, p)) { return map.getStackMetrics().merge(s, p); } else { return null; } } public Object visitDefault(GamePiece piece) { if (piece.getPosition().equals(pt) && map.getStackMetrics().isStackingEnabled() && !Boolean.TRUE.equals(p.getProperty(Properties.NO_STACK)) && !Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_ME)) && !Boolean.TRUE.equals(piece.getProperty(Properties.NO_STACK)) && map.getPieceCollection().canMerge(piece, p)) { return map.getStackMetrics().merge(piece, p); } else { return null; } } } /** * The component that represents the map itself */ public static class View extends JPanel { private static final long serialVersionUID = 1L; protected Map map; public View(Map m) { setFocusTraversalKeysEnabled(false); map = m; } public void paint(Graphics g) { // Don't draw the map until the game is updated. if (GameModule.getGameModule().getGameState().isUpdating()) { return; } Rectangle r = getVisibleRect(); g.setColor(map.bgColor); g.fillRect(r.x, r.y, r.width, r.height); map.paintRegion(g, r); } public void update(Graphics g) { // To avoid flicker, don't clear the display first * paint(g); } public Dimension getPreferredSize() { return map.getPreferredSize(); } public Map getMap() { return map; } } }