/* * $Id$ * * Copyright (c) 2000-2012 by Rodney Kinney, 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.map.boardPicker; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Component; import java.awt.Composite; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.io.File; import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import org.jdesktop.animation.timing.Animator; import org.jdesktop.animation.timing.TimingTargetAdapter; import VASSAL.build.AbstractConfigurable; import VASSAL.build.Buildable; import VASSAL.build.Builder; import VASSAL.build.GameModule; import VASSAL.build.module.GameComponent; import VASSAL.build.module.Map; import VASSAL.build.module.documentation.HelpFile; import VASSAL.build.module.map.boardPicker.board.HexGrid; import VASSAL.build.module.map.boardPicker.board.MapGrid; import VASSAL.build.module.map.boardPicker.board.RegionGrid; import VASSAL.build.module.map.boardPicker.board.SquareGrid; import VASSAL.build.module.map.boardPicker.board.ZonedGrid; import VASSAL.build.module.map.boardPicker.board.mapgrid.GridContainer; import VASSAL.command.Command; import VASSAL.configure.ColorConfigurer; import VASSAL.configure.SingleChildInstance; import VASSAL.configure.VisibilityCondition; import VASSAL.i18n.Resources; import VASSAL.tools.ErrorDialog; import VASSAL.tools.image.ImageIOException; import VASSAL.tools.image.ImageTileSource; import VASSAL.tools.imageop.ImageOp; import VASSAL.tools.imageop.Op; import VASSAL.tools.imageop.Repainter; import VASSAL.tools.imageop.ScaleOp; import VASSAL.tools.imageop.SourceOp; public class Board extends AbstractConfigurable implements GridContainer { /** * A Board is a piece of a Map. * A Map can cantain a set of boards layed out in a rectangular grid. */ public static final String NAME = "name"; public static final String IMAGE = "image"; public static final String WIDTH = "width"; public static final String HEIGHT = "height"; public static final String COLOR = "color"; public static final String REVERSIBLE = "reversible"; protected Point pos = new Point(0, 0); protected Rectangle boundaries = new Rectangle(0, 0, 500, 500); protected String imageFile; protected boolean reversible = false; protected boolean reversed = false; protected boolean fixedBoundaries = false; protected Color color = null; protected MapGrid grid = null; protected Map map; protected double magnification = 1.0; @Deprecated protected String boardName = "Board 1"; @Deprecated protected Image boardImage; protected SourceOp boardImageOp; protected ScaleOp scaledImageOp; public Board() { } /** * @return this <code>Board</code>'s {@link Map}. * Until a game is started that is using this board, the map will be null. */ public Map getMap() { return map; } public void setMap(Map map) { this.map = map; } public String getLocalizedName() { final String s = getLocalizedConfigureName(); return s != null ? s : ""; } public String getName() { final String s = getConfigureName(); return s != null ? s : ""; } public void addTo(Buildable b) { validator = new SingleChildInstance(this, MapGrid.class); } public void removeFrom(Buildable b) { } public String[] getAttributeNames() { return new String[] { NAME, IMAGE, REVERSIBLE, WIDTH, HEIGHT, COLOR }; } public String[] getAttributeDescriptions() { return new String[] { Resources.getString(Resources.NAME_LABEL), Resources.getString("Editor.Board.image"), //$NON-NLS-1$ Resources.getString("Editor.Board.reverse"), //$NON-NLS-1$ Resources.getString("Editor.Board.width"), //$NON-NLS-1$ Resources.getString("Editor.Board.height"), //$NON-NLS-1$ Resources.getString(Resources.COLOR_LABEL), }; } public static String getConfigureTypeName() { return Resources.getString("Editor.Board.component_type"); //$NON-NLS-1$ } public Class<?>[] getAttributeTypes() { return new Class<?>[] { String.class, Image.class, Boolean.class, Integer.class, Integer.class, Color.class }; } public VisibilityCondition getAttributeVisibility(String name) { if (REVERSIBLE.equals(name)) { return new VisibilityCondition() { public boolean shouldBeVisible() { return imageFile != null; } }; } else if (WIDTH.equals(name) || HEIGHT.equals(name) || COLOR.equals(name)) { return new VisibilityCondition() { public boolean shouldBeVisible() { return imageFile == null; } }; } else { return null; } } public String getAttributeValueString(String key) { if (NAME.equals(key)) { return getConfigureName(); } else if (IMAGE.equals(key)) { return imageFile; } else if (WIDTH.equals(key)) { return imageFile == null ? String.valueOf(boundaries.width) : null; } else if (HEIGHT.equals(key)) { return imageFile == null ? String.valueOf(boundaries.height) : null; } else if (COLOR.equals(key)) { return imageFile == null ? ColorConfigurer.colorToString(color) : null; } else if (REVERSIBLE.equals(key)) { return String.valueOf(reversible); } return null; } public void setAttribute(String key, Object val) { if (NAME.equals(key)) { setConfigureName((String) val); } else if (IMAGE.equals(key)) { if (val instanceof File) { val = ((File) val).getName(); } imageFile = (String) val; if (imageFile == null || imageFile.trim().length() == 0) { boardImageOp = null; } else { final ImageTileSource ts = GameModule.getGameModule().getImageTileSource(); boolean tiled = false; try { tiled = ts.tileExists("images/" + imageFile, 0, 0, 1.0); } catch (ImageIOException e) { // ignore, not tiled } if (tiled) { boardImageOp = Op.loadLarge(imageFile); } else { boardImageOp = Op.load(imageFile); } } } else if (WIDTH.equals(key)) { if (val instanceof String) { val = Integer.valueOf((String) val); } if (val != null) { boundaries.setSize(((Integer) val).intValue(), boundaries.height); } } else if (HEIGHT.equals(key)) { if (val instanceof String) { val = Integer.valueOf((String) val); } if (val != null) { boundaries.setSize(boundaries.width, ((Integer) val).intValue()); } } else if (COLOR.equals(key)) { if (val instanceof String) { val = ColorConfigurer.stringToColor((String) val); } color = (Color) val; } else if (REVERSIBLE.equals(key)) { if (val instanceof String) { val = Boolean.valueOf((String) val); } reversible = ((Boolean) val).booleanValue(); } } public Class<?>[] getAllowableConfigureComponents() { return new Class<?>[] { HexGrid.class, SquareGrid.class, RegionGrid.class, ZonedGrid.class }; } public void draw(Graphics g, int x, int y, double zoom, Component obs) { drawRegion(g, new Point(x,y), new Rectangle(x, y, Math.round((float) zoom*boundaries.width), Math.round((float) zoom*boundaries.height)), zoom, obs); } private ConcurrentMap<Point,Future<BufferedImage>> requested = new ConcurrentHashMap<Point,Future<BufferedImage>>(); private java.util.Map<Point,Float> alpha = new ConcurrentHashMap<Point,Float>(); private ConcurrentMap<Point,Future<BufferedImage>> o_requested = new ConcurrentHashMap<Point,Future<BufferedImage>>(); private static Comparator<Point> tileOrdering = new Comparator<Point>() { public int compare(Point t1, Point t2) { if (t1.y < t2.y) return -1; if (t1.y > t2.y) return 1; return t1.x - t2.x; } }; protected void drawTile(Graphics g, Future<BufferedImage> fim, int tx, int ty, Component obs) { try { g.drawImage(fim.get(), tx, ty, obs); } catch (CancellationException e) { // FIXME: bug until we permit cancellation ErrorDialog.bug(e); } catch (InterruptedException e) { // This happens if taking a snapshot of the map is cancelled. // FIXME: Can we handle this in ImageSaver instead? } catch (ExecutionException e) { if (!Op.handleException(e)) ErrorDialog.bug(e); } } public void drawRegion(final Graphics g, final Point location, Rectangle visibleRect, double zoom, final Component obs) { zoom *= magnification; final Rectangle bounds = new Rectangle(location.x, location.y, Math.round(boundaries.width * (float) zoom), Math.round(boundaries.height * (float) zoom)); if (visibleRect.intersects(bounds)) { visibleRect = visibleRect.intersection(bounds); if (boardImageOp != null) { final ImageOp op; if (zoom == 1.0 && !reversed) { op = boardImageOp; } else { if (scaledImageOp == null || scaledImageOp.getScale() != zoom) { scaledImageOp = Op.scale(boardImageOp, zoom); } op = reversed ? Op.rotate(scaledImageOp, 180) : scaledImageOp; } final Rectangle r = new Rectangle(visibleRect.x - location.x, visibleRect.y - location.y, visibleRect.width, visibleRect.height); final int ow = op.getTileWidth(); final int oh = op.getTileHeight(); final Point[] tiles = op.getTileIndices(r); for (Point tile : tiles) { // find tile position final int tx = location.x + tile.x*ow; final int ty = location.y + tile.y*oh; // find actual tile size final int tw = Math.min(ow, location.x+bounds.width-tx); final int th = Math.min(oh, location.y+bounds.height-ty); final Repainter rep = obs == null ? null : new Repainter(obs, tx, ty, tw, th); try { final Future<BufferedImage> fim = op.getFutureTile(tile.x, tile.y, rep); if (obs == null) { drawTile(g, fim, tx, ty, obs); } else { if (fim.isDone()) { // FIXME: We check whether the observer here is a map view in order to // avoid mixing requests (and fade-in) between maps and their overview // maps. This is a kludge which should be fixed when model-view // separation happens. if ((map != null) && (obs == map.getView())) { if (requested.containsKey(tile)) { requested.remove(tile); final Point t = tile; final Animator a = new Animator(100, new TimingTargetAdapter() { @Override public void timingEvent(float fraction) { alpha.put(t, fraction); obs.repaint(tx, ty, tw, th); } } ); a.setResolution(20); a.start(); } else { Float a = alpha.get(tile); if (a != null && a < 1.0f) { final Graphics2D g2d = (Graphics2D) g; final Composite oldComp = g2d.getComposite(); g2d.setComposite( AlphaComposite.getInstance(AlphaComposite.SRC_OVER, a)); drawTile(g2d, fim, tx, ty, obs); g2d.setComposite(oldComp); } else { alpha.remove(tile); drawTile(g, fim, tx, ty, obs); } } } else { if (o_requested.containsKey(tile)) { o_requested.remove(tile); obs.repaint(tx, ty, tw, th); } else { drawTile(g, fim, tx, ty, obs); } } } else { if ((map != null) && (obs == map.getView())) { requested.putIfAbsent(tile, fim); } else { o_requested.putIfAbsent(tile, fim); } } } } // FIXME: should getTileFuture() throw these? Yes, probably, because it's // synchronous when obs is null. catch (CancellationException e) { // FIXME: bug until we permit cancellation ErrorDialog.bug(e); } catch (ExecutionException e) { // FIXME: bug until we figure out why getTileFuture() throws this ErrorDialog.bug(e); } } if ((map != null) && (obs == map.getView())) { for (Point tile : requested.keySet().toArray(new Point[0])) { if (Arrays.binarySearch(tiles, tile, tileOrdering) < 0) { requested.remove(tile); } } } else { for (Point tile : o_requested.keySet().toArray(new Point[0])) { if (Arrays.binarySearch(tiles, tile, tileOrdering) < 0) { o_requested.remove(tile); } } } /* final StringBuilder sb = new StringBuilder(); for (Point tile : requested.keySet().toArray(new Point[0])) { if (Arrays.binarySearch(tiles, tile, tileOrdering) < 0) { final Future<Image> fim = requested.remove(tile); if (!fim.isDone()) { sb.append("(") .append(tile.x) .append(",") .append(tile.y) .append(") "); } } } if (sb.length() > 0) { sb.insert(0, "cancelling: ").append("\n"); System.out.print(sb.toString()); } */ } else if (color != null) { g.setColor(color); g.fillRect(visibleRect.x, visibleRect.y, visibleRect.width, visibleRect.height); } if (grid != null) { grid.draw(g, bounds, visibleRect, zoom, reversed); } } } @Deprecated public synchronized Image getScaledImage(double zoom, Component obs) { try { final ImageOp sop = Op.scale(boardImageOp, zoom); return (reversed ? Op.rotate(sop, 180) : sop).getImage(null); } catch (CancellationException e) { ErrorDialog.bug(e); } catch (InterruptedException e) { ErrorDialog.bug(e); } catch (ExecutionException e) { ErrorDialog.bug(e); } return null; } public void setReversed(boolean val) { if (reversible) { if (reversed != val) { reversed = val; scaledImageOp = null; // get a new rendered version on next paint } } } public boolean isReversed() { return reversed; } /** * If this board is reversed, return the location in un-reversed coordinates */ public Point localCoordinates(Point p) { if (reversed) { p.x = bounds().width - p.x; p.y = bounds().height - p.y; } if (magnification != 1.0) { p.x = (int) Math.round(p.x/magnification); p.y = (int) Math.round(p.y/magnification); } return p; } /** * If this board is reversed, return the location in reversed coordinates */ public Point globalCoordinates(Point p) { if (magnification != 1.0) { p.x = (int) Math.round(p.x*magnification); p.y = (int) Math.round(p.y*magnification); } if (reversed) { p.x = bounds().width - p.x; p.y = bounds().height - p.y; } return p; } public void setGrid(MapGrid mg) { grid = mg; } public void removeGrid(MapGrid grid) { if (this.grid == grid) { this.grid = null; } } public Board getBoard() { return this; } public Dimension getSize() { return bounds().getSize(); } public boolean contains(Point p) { return bounds().contains(p); } public MapGrid getGrid() { return grid; } public Board copy() { Board b = new Board(); b.build(getBuildElement(Builder.createNewDocument())); return b; } /** * @deprecated Images are now fixed automagically using {@link ImageOp}s. */ @Deprecated public void fixImage(Component map) { } /** * @deprecated Images are now fixed automagically using {@link ImageOp}s. */ @Deprecated public void fixImage() { } public String locationName(Point p) { return grid == null ? null : grid.locationName(localCoordinates(p)); } public String localizedLocationName(Point p) { return grid == null ? null : grid.localizedLocationName(localCoordinates(p)); } public Point snapTo(Point p) { return grid == null ? p : globalCoordinates(grid.snapTo(localCoordinates(p))); } /** * @return true if the given point may not be a local location. * I.e., if this grid will attempt to snap it to the nearest grid location. */ public boolean isLocationRestricted(Point p) { return grid == null ? false : grid.isLocationRestricted(localCoordinates(p)); } public String fileName() { return imageFile; } /** * @return Position of this board relative to the other boards (0,0) is the upper left, (0,1) is to the right, etc. */ public Point relativePosition() { return pos; } /** * @return The (read-only) boundaries of this Board within the overall Map */ public Rectangle bounds() { if (imageFile != null && boardImageOp != null && !fixedBoundaries) { boundaries.setSize(boardImageOp.getSize()); if (magnification != 1.0) { boundaries.setSize((int)Math.round(magnification*boundaries.width), (int)Math.round(magnification*boundaries.height)); } fixedBoundaries = true; } return new Rectangle(boundaries); } /** * @deprecated Bounds are now fixed automagically by {@link ImageOp}s. */ @Deprecated protected void fixBounds() { } /** * Translate the location of the board by the given number of pixels * * @see #bounds() */ public void translate(int x, int y) { boundaries.translate(x, y); } /** * Set the location of this board * * @see #bounds() */ public void setLocation(int x, int y) { boundaries.setLocation(x, y); } public HelpFile getHelpFile() { return HelpFile.getReferenceManualPage("Board.htm"); } /** * Removes board images from the {@link VASSAL.tools.DataArchive} cache * @deprecated Board images are removed automatically now, when under * memory pressure. */ @Deprecated public void cleanUp() { if (imageFile != null) { GameModule.getGameModule().getDataArchive().unCacheImage("images/" + imageFile); } if (boardImage != null) { GameModule.getGameModule().getDataArchive().unCacheImage(boardImage); boardImage = null; } } /** * Cleans up {@link Board}s (by invoking {@link Board#cleanUp}) when a * game is closed * @deprecated Only used to cleanup <code>Board</code> images, which * is now handled automatically by the cache. */ @Deprecated public static class Cleanup implements GameComponent { private static Cleanup instance; private Set<Board> toClean = new HashSet<Board>(); private boolean gameStarted = false; public static void init() { if (instance == null) { instance = new Cleanup(); } } private Cleanup() { GameModule.getGameModule().getGameState().addGameComponent(this); } public static Cleanup getInstance() { return instance; } /** * Mark this board as needing to be cleaned up when the game is closed * * @param b */ public void addBoard(Board b) { toClean.add(b); } public Command getRestoreCommand() { return null; } public void setup(boolean gameStarting) { if (gameStarted && !gameStarting) { for (Board board : toClean) { board.cleanUp(); } toClean.clear(); } gameStarted = gameStarting; } } public double getMagnification() { return magnification; } public void setMagnification(double magnification) { this.magnification = magnification; fixedBoundaries = false; bounds(); } }