package com.kartoflane.ftl.floorgen; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Scanner; import javax.imageio.ImageIO; import com.kartoflane.ftl.layout.ShipLayout; /** * A "factory" of floor images for ships' layouts.<br> * <br> * Can generate a matching floor image for any sanely constructed room layout (ie. without * any airlocks in the middle of a room, etc). Does not account for doors linked to * non-neighbouring rooms (ie. won't draw indentations for them).<br> * <br> * Settings other than default may end up producing floor images of questionable quality.<br> * <br> * Example usage: <code> * <pre> * FloorImageFactory fif = new FloorImageFactory(); * // optional factory configuration * BufferedImage floorImage = fif.generateFloorImage(layoutFile); * ImageIO.write(floorImage, "PNG", new File("floor_image.png")); * </pre> * </code> * * @author kartoFlane * */ public class FloorImageFactory { // @formatter:off private static final int cellSize = 35; private static final Color transparentColor = new Color(0, 0, 0, 0); // @formatter:on private int floorMargin; private int borderWidth; private int cornerSize; private Color floorColor; private Color borderColor; /** * Creates a new FloorImageFactory object with default settings. */ public FloorImageFactory() { floorMargin = 4; borderWidth = 2; cornerSize = 2; floorColor = new Color(0x64696F); borderColor = Color.BLACK; } /* * ================================ * Getters & setters for properties */ /** * Sets the distance between the border and room walls.<br> * Default: 4 */ public void setFloorMargin(int margin) { if (margin < 0) throw new IllegalArgumentException("Argument must be non-negative!"); floorMargin = margin; } public int getFloorMargin() { return floorMargin; } /** * Sets width of the border.<br> * Default: 2 */ public void setBorderWidth(int border) { if (border < 0) throw new IllegalArgumentException("Argument must be non-negative!"); borderWidth = border; } public int getBorderWidth() { return borderWidth; } /** * Sets size of corners. Larger value = smaller corners.<br> * Default: 2 */ public void setCornerSize(int size) { if (size < 0) throw new IllegalArgumentException("Argument must be non-negative!"); cornerSize = size; } public int getCornerSize() { return cornerSize; } /** * Sets color of the floor image's body. Can be null for transparent color.<br> * Default: <tt>ARGB[0xFF64696F]</tt> */ public void setFloorColor(Color c) { if (c == null) c = transparentColor; floorColor = c; } public Color getFloorColor() { return floorColor; } /** * Sets color of the border. Can be null for transparent color.<br> * Default: <tt>RGBA[0, 0, 0, 255]</tt> */ public void setBorderColor(Color c) { if (c == null) c = transparentColor; borderColor = c; } public Color getBorderColor() { return borderColor; } /* * ============== * Actual methods */ /** * Attempts to generate a matching floor image for the specified layout.<br> * <br> * The returned image can be later saved using<br> * {@link ImageIO ImageIO.write(image, "PNG", new File("C:/yourImageName.PNG"));} * * @param layoutFile * the .txt room layout file * * @throws FileNotFoundException * when the file specified in the argument could not be found or accessed * @throws IOException * when an IOException occurs * @throws IllegalArgumentException * when the file contains syntax errors, or the argument is null */ public BufferedImage generateFloorImage(File layoutFile) throws FileNotFoundException, IllegalArgumentException, IOException { if (layoutFile == null) { throw new IllegalArgumentException("Argument must not be null!"); } InputStream is = null; BufferedImage result = null; try { is = new FileInputStream(layoutFile); result = generateFloorImage(is); } finally { if (is != null) is.close(); } return result; } /** * Attempts to generate a matching floor image for the specified layout.<br> * <br> * The returned image can be later saved using<br> * {@link ImageIO ImageIO.write(image, "PNG", new File("C:/yourImageName.PNG"));}<br> * <br> * <b>This method does NOT close the stream.</b> * * @param is * input stream for the file containing the ship's room layout * @return the floor image * * @throws IllegalArgumentException * when the file contains syntax errors, or argument is null */ public BufferedImage generateFloorImage(InputStream is) throws IllegalArgumentException { if (is == null) { throw new IllegalArgumentException("Argument must not be null!"); } return generateFloorImage(loadLayout(is)); } /** * Attempts to generate a matching floor image for the specified layout.<br> * <br> * The returned image can be later saved using<br> * {@link ImageIO ImageIO.write(image, "PNG", new File("C:/yourImageName.PNG"));} * * @param layout * <tt>ShipLayout</tt> object representing the ship's layout * @return the floor image * @throws IllegalArgumentException * */ public BufferedImage generateFloorImage(ShipLayout layout) { if (layout == null) { throw new IllegalArgumentException("Argument must not be null!"); } return generateFloorImage(new Layout(layout)); } /** * Attempts to generate a matching floor image for the specified layout.<br> * <br> * The returned image can be later saved using<br> * {@link ImageIO ImageIO.write(image, "PNG", new File("C:/yourImageName.PNG"));} * * @param layout * <tt>Layout</tt> object representing the ship's .txt layout * @return the floor image */ private BufferedImage generateFloorImage(Layout layout) { if (layout == null) { throw new IllegalArgumentException("Argument must not be null!"); } final int offset = floorMargin + borderWidth; // TYPE_INT_ARGB specifies the image format: 8-bit RGBA packed into integer pixels final BufferedImage result = new BufferedImage(layout.getWidth() * cellSize + 2 * offset, layout.getHeight() * cellSize + 2 * offset, BufferedImage.TYPE_INT_ARGB); final Graphics2D gc = result.createGraphics(); // Ensure that the background is transparent gc.setBackground(transparentColor); gc.clearRect(0, 0, result.getWidth(), result.getHeight()); // Create rectangles representing each individual tile and store them in a matrix for ease of referencing final Graph graph = layout.getTileGraph(); final Rectangle[][] tiles = new Rectangle[layout.getWidth()][layout.getHeight()]; for (Point v : graph) { if (graph.hasEdge(v, v.x + 1, v.y) && graph.hasEdge(v, v.x, v.y + 1) && graph.hasEdge(v, v.x + 1, v.y + 1)) tiles[v.x][v.y] = new Rectangle(offset + v.x * cellSize, offset + v.y * cellSize, cellSize, cellSize); } for (int x = 0; x < tiles.length; x++) { for (int y = 0; y < tiles[0].length; y++) { Rectangle tile = tiles[x][y]; if (tile != null) { // Fill the tile with color gc.setColor(floorColor); gc.fillRect(tile.x, tile.y, tile.width, tile.height); // Draw 'flaps' at the tile's sides, if there are no bordernig tiles // Also draw airlock corners if (getTile(tiles, x, y - 1) == null) { // Doesn't have a neighbouring tile above if (layout.hasAirlockAt(x, y, true)) { int cx = tile.x; int cy = tile.y; // If there's a wall, offset to prevent overlap if (getTile(tiles, x - 1, y - 1) != null) cx += floorMargin; // Don't draw if there's a neighbouring airlock, unless on a double dent if (layout.isDoubleDent(x, y) || !layout.hasAirlockAt(x - 1, y, true)) drawTopRightCorner(gc, cx, cy); cx = tile.x + tile.width; // If there's a wall, offset to prevent overlap if (getTile(tiles, x + 1, y - 1) != null) cx -= floorMargin; // Don't draw if there's a neighbouring airlock, unless on a double dent if (layout.isDoubleDent(x + 1, y) || !layout.hasAirlockAt(x + 1, y, true)) drawTopLeftCorner(gc, cx, cy); } else { gc.setColor(borderColor); // Dents & double dents need to be drawn with slight offsets to prevent overlapping if (layout.isDent(x, y) || layout.isDoubleDent(x, y)) { gc.fillRect(tile.x + floorMargin, tile.y - floorMargin - borderWidth, tile.width - floorMargin, floorMargin + borderWidth); } else { gc.fillRect(tile.x, tile.y - floorMargin - borderWidth, tile.width, floorMargin + borderWidth); } gc.setColor(floorColor); gc.fillRect(tile.x, tile.y - floorMargin, tile.width, floorMargin); } } if (getTile(tiles, x - 1, y) == null) { // Doesn't have a neighbouring tile to the left if (layout.hasAirlockAt(x, y, false)) { int cx = tile.x; int cy = tile.y; // Two airlocks at a dent get drawn correctly, but leave empty space between their // corners. Fill that area with color if (layout.hasAirlockAt(x - 1, y, true)) { gc.setColor(floorColor); gc.fillRect(cx - floorMargin, cy, floorMargin, floorMargin); } if (layout.hasAirlockAt(x - 1, y + 1, true)) { gc.setColor(floorColor); gc.fillRect(cx - floorMargin, cy + tile.height - floorMargin, floorMargin, floorMargin); } // If there's a wall, offset to prevent overlap if (getTile(tiles, x - 1, y - 1) != null) cy += floorMargin; // Don't draw if there's a neighbouring airlock, unless on a double denet if (layout.isDoubleDent(x, y) || !layout.hasAirlockAt(x, y - 1, false)) drawBottomLeftCorner(gc, cx, cy); cy = tile.y + tile.height; // If there's a wall, offset to prevent overlap if (getTile(tiles, x - 1, y + 1) != null) cy -= floorMargin; // Don't draw if there's a neighbouring airlock, unless on a double dent if (layout.isDoubleDent(x, y + 1) || !layout.hasAirlockAt(x, y + 1, false)) drawTopLeftCorner(gc, cx, cy); } else { boolean isDent = layout.isDent(x, y) || layout.isDoubleDent(x, y); boolean isLowerDent = layout.isDent(x, y + 1) || layout.isDoubleDent(x, y + 1); // Dents & double dents need to be drawn with slight offsets to prevent overlapping gc.setColor(borderColor); if (isDent && isLowerDent) { gc.fillRect(tile.x - floorMargin - borderWidth, tile.y + floorMargin, floorMargin + borderWidth, tile.height - floorMargin * 2); } else if (isDent) { gc.fillRect(tile.x - floorMargin - borderWidth, tile.y + floorMargin, floorMargin + borderWidth, tile.height - floorMargin); } else if (isLowerDent) { gc.fillRect(tile.x - floorMargin - borderWidth, tile.y, floorMargin + borderWidth, tile.height - floorMargin); } else { gc.fillRect(tile.x - floorMargin - borderWidth, tile.y, floorMargin + borderWidth, tile.height); } gc.setColor(floorColor); gc.fillRect(tile.x - floorMargin, tile.y, floorMargin, tile.height); } } if (getTile(tiles, x, y + 1) == null) { // Doesn't have a neighbouring tile below if (layout.hasAirlockAt(x, y + 1, true)) { int cx = tile.x; int cy = tile.y + tile.height; // If there's a wall, offset to prevent overlap if (getTile(tiles, x - 1, y + 1) != null) cx += floorMargin; // Don't draw if there's a neighbouring airlock, unless on a double dent if (layout.isDoubleDent(x, y + 1) || !layout.hasAirlockAt(x - 1, y + 1, true)) drawBottomRightCorner(gc, cx, cy); cx = tile.x + tile.width; // If there's a wall, offset to prevent overlap if (getTile(tiles, x + 1, y + 1) != null) cx -= floorMargin; // Don't draw if there's a neighbouring airlock, unless on a double dent if (layout.isDoubleDent(x + 1, y + 1) || !layout.hasAirlockAt(x + 1, y + 1, true)) drawBottomLeftCorner(gc, cx, cy); } else { gc.setColor(borderColor); // Dents & double dents need to be drawn with slight offsets to prevent overlapping if (layout.isDent(x, y + 1) || layout.isDoubleDent(x, y + 1)) { gc.fillRect(tile.x + floorMargin, tile.y + tile.height, tile.width - floorMargin, floorMargin + borderWidth); } else { gc.fillRect(tile.x, tile.y + tile.height, tile.width, floorMargin + borderWidth); } gc.setColor(floorColor); gc.fillRect(tile.x, tile.y + tile.height, tile.width, floorMargin); } } if (getTile(tiles, x + 1, y) == null) { // Doesn't have a neighbouring tile to the right if (layout.hasAirlockAt(x + 1, y, false)) { int cx = tile.x + tile.width; int cy = tile.y; // Two airlocks at a dent get drawn correctly, but leave empty space between their // corners. Fill that area with color if (layout.hasAirlockAt(x + 1, y, true)) { gc.setColor(floorColor); gc.fillRect(cx, cy, floorMargin, floorMargin); } if (layout.hasAirlockAt(x + 1, y + 1, true)) { gc.setColor(floorColor); gc.fillRect(cx, cy + tile.height - floorMargin, floorMargin, floorMargin); } // If there's a wall, offset to prevent overlap if (getTile(tiles, x + 1, y - 1) != null) cy += floorMargin; // Don't draw if there's a neighbouring airlock, unless on a double dent if (layout.isDoubleDent(x + 1, y) || !layout.hasAirlockAt(x + 1, y - 1, false)) drawBottomRightCorner(gc, cx, cy); cy = tile.y + tile.height; // If there's a wall, offset to prevent overlap if (getTile(tiles, x + 1, y + 1) != null) cy -= floorMargin; // Don't draw if there's a neighbouring airlock, unless on a double dent if (layout.isDoubleDent(x + 1, y + 1) || !layout.hasAirlockAt(x + 1, y + 1, false)) drawTopRightCorner(gc, cx, cy); } else { gc.setColor(borderColor); // Overlaps caused by dents & double dents drawn here are obscured by other tiles gc.fillRect(tile.x + tile.width, tile.y, floorMargin + borderWidth, tile.height); gc.setColor(floorColor); gc.fillRect(tile.x + tile.width, tile.y, floorMargin, tile.height); } } // Draw corners if (layout.isCorner(x, y) && graph.hasEdge(x, y, x + 1, y + 1)) drawTopLeftCorner(gc, tile.x, tile.y); if (layout.isCorner(x + 1, y) && graph.hasEdge(x + 1, y, x, y + 1)) drawTopRightCorner(gc, tile.x + tile.width, tile.y); if (layout.isCorner(x, y + 1) && graph.hasEdge(x, y + 1, x + 1, y)) drawBottomLeftCorner(gc, tile.x, tile.y + tile.height); if (layout.isCorner(x + 1, y + 1) && graph.hasEdge(x + 1, y + 1, x, y)) drawBottomRightCorner(gc, tile.x + tile.width, tile.y + tile.height); } } } return result; } private void drawTopLeftCorner(Graphics2D gc, int cx, int cy) { gc.setColor(borderColor); gc.fillPolygon(new int[] { cx, cx - floorMargin - borderWidth, cx - floorMargin - borderWidth, cx - cornerSize - 1, cx }, new int[] { cy, cy, cy - cornerSize - 1, cy - floorMargin - borderWidth, cy - floorMargin - borderWidth }, 5); gc.setColor(floorColor); gc.fillPolygon(new int[] { cx, cx - floorMargin, cx - floorMargin, cx - cornerSize, cx }, new int[] { cy, cy, cy - cornerSize, cy - floorMargin, cy - floorMargin }, 5); } private void drawTopRightCorner(Graphics2D gc, int cx, int cy) { gc.setColor(borderColor); gc.fillPolygon(new int[] { cx, cx, cx + cornerSize + 1, cx + floorMargin + borderWidth, cx + floorMargin + borderWidth }, new int[] { cy, cy - floorMargin - borderWidth, cy - floorMargin - borderWidth, cy - cornerSize - 1, cy }, 5); gc.setColor(floorColor); gc.fillPolygon(new int[] { cx, cx, cx + cornerSize, cx + floorMargin, cx + floorMargin }, new int[] { cy, cy - floorMargin, cy - floorMargin, cy - cornerSize, cy }, 5); } private void drawBottomLeftCorner(Graphics2D gc, int cx, int cy) { gc.setColor(borderColor); gc.fillPolygon(new int[] { cx, cx, cx - cornerSize, cx - floorMargin - borderWidth, cx - floorMargin - borderWidth }, new int[] { cy, cy + floorMargin + borderWidth, cy + floorMargin + borderWidth, cy + cornerSize, cy }, 5); gc.setColor(floorColor); gc.fillPolygon(new int[] { cx, cx, cx - cornerSize, cx - floorMargin, cx - floorMargin }, new int[] { cy, cy + floorMargin, cy + floorMargin, cy + cornerSize - 1, cy }, 5); } private void drawBottomRightCorner(Graphics2D gc, int cx, int cy) { gc.setColor(borderColor); gc.fillPolygon(new int[] { cx, cx, cx + cornerSize, cx + floorMargin + borderWidth, cx + floorMargin + borderWidth }, new int[] { cy, cy + floorMargin + borderWidth, cy + floorMargin + borderWidth, cy + cornerSize, cy }, 5); gc.setColor(floorColor); gc.fillPolygon(new int[] { cx, cx, cx + cornerSize - 1, cx + floorMargin, cx + floorMargin }, new int[] { cy, cy + floorMargin, cy + floorMargin, cy + cornerSize - 1, cy }, 5); } private Layout loadLayout(InputStream is) throws IllegalArgumentException { List<Rectangle> rooms = new ArrayList<Rectangle>(); List<Door> airlocks = new ArrayList<Door>(); Scanner sc = new Scanner(is); while (sc.hasNext()) { String line = sc.nextLine(); if (line.trim().equals("")) continue; LayoutObjects layoutObject = null; try { layoutObject = LayoutObjects.valueOf(line); } catch (IllegalArgumentException e) { try { // Not a layout object -- has to be integer value // Check if it is, in fact, a number to verify syntax Integer.parseInt(line); continue; } catch (NumberFormatException ex) { // Not a layout object or integer value - syntax error // Intercept the exception to throw a more meaningful one... throw new IllegalArgumentException("TXT layout syntax error on line: \n" + line); } } switch (layoutObject) { case X_OFFSET: case Y_OFFSET: case HORIZONTAL: case VERTICAL: case ELLIPSE: // Ignore break; case ROOM: // Id sc.nextInt(); // x, y, w, h rooms.add(new Rectangle(sc.nextInt(), sc.nextInt(), sc.nextInt(), sc.nextInt())); break; case DOOR: // x, y Point p = new Point(sc.nextInt(), sc.nextInt()); // Left Id int l = sc.nextInt(); // Right Id int r = sc.nextInt(); // If either ID is -1, then the door is an airlock if (l == -1 || r == -1) { // Horizontal if 0, vertical if 1 airlocks.add(new Door(p.x, p.y, sc.nextInt() == 0)); } break; } } return new Layout(rooms, airlocks); } private Rectangle getTile(Rectangle[][] matrix, int x, int y) { if (x < 0 || x >= matrix.length || y < 0 || y >= matrix[x].length) return null; return matrix[x][y]; } }