package edu.gatech.cs2335.lemmings.graphics; import java.util.List; import java.util.Vector; //import java.net.URL; import java.awt.Point; import java.awt.Color; import java.awt.Graphics; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.image.Raster; import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; /** * The tileset class simplifies loading bitmap files that contain * several tiles. It is a fairly flexible class, but it does impose * certain restrictions on the way the file is organized. First of * all, there needs to be a set of control pixels in the upper-right * corner of the image. The control pixels start at the coordinate * (width-1, 0) and go down from there in the following order: * transparency pixel, empty-region pixel, outside anchor pixel, * used-region pixel, inside anchor pixel; five in all. The * transparency pixel is used for color-keying, and all the pixels of * that color in the image will be made transparent. The outside and * inside anchor pixels do the same thing currently, and are used to * denote the image anchor. When the tile is blitted using the * putTile method, the anchor has the coordinates passed in to the * method, and the rest of the image is drawn relatively to it. In * other words, suppose we blit to rectangular tiles using the same * target coordinates. One of the tiles has its anchor in the * top-left corner, and the other - in the center. Then the center of * the latter tile will coincide with the top-left corner of the * former tile. The empty-region and used-region pixels are used to * denote where the useful areas of the tile are, and which ones can * be discarded. The reason for that is that the tiles in the tileset * file have to be arranged in a table. In other words, all tiles in * one column have to be the same width, and all the tiles in one row * have to be the same height. That is not to say, however, that the * width and the height have to be the equal, or that the tiles in * different rows have to have the same height, and in different * columns - the same width. Suppose a game has only two tiles. We * can arrange them in a column. One of the tiles is square, and the * other - a very thin rectangle. Then, we could mark the whole first * tile as used, and for the second tile - only the space that is * actually used up. Since we arranged the tiles in a column, the * widths of the tiles have to be the same. However, we can mark the * unused areas of the thin tile with the unused-area pixel color, * and they will be discarded when blitting. * *
 * Revision History:
 *     v1.0 (Mar. 11, 2004) - Created the TileSet class
 * 
* * @author Vladimir Urazov * @version Version 1.0, Mar. 11, 2004 */ public class TileSet { /** * Show debug output? */ protected static final boolean DEBUG = false; /** * Show lots of debug output? */ protected static final boolean VERBOSE = false; /** * The list of informations pertaining to the frames in this set. */ private FrameInformation[] frameList; /** * The name of the file we want to load the image from, in case it * needs to be reloaded somehow. */ private String fileName; /** * The image, where the whole tileset is stored. */ private BufferedImage framesetImage; /** * Creates a new TileSet instance. */ public TileSet() { frameList = null; fileName = null; framesetImage = null; } /** * Loads the tileset from a file. Returns true upon success or false * upon failure. * * @param name a String value * @return a boolean value */ public boolean loadTileset(String name) { //Dimensions of the image int width = 0; int height = 0; //Number of tiles horizontally and vertically // int xCount = 0; // int yCount = 0; //Lists of coordinates for the tile bounds List horizontalTileBounds = new Vector(); List verticalTileBounds = new Vector(); //Color control Color transparentColor = null; Color boundColor = null; Color anchorColor = null; Color insideColor = null; Color insideAnchorColor = null; Color testColor = null; //Image info Raster tilesetRaster = null; ColorSpace tilesetSpace = null; //Miscellaneous temporaries int x = 0; int y = 0; int row = 0; int col = 0; int currentTile = 0; // boolean found = false; //First, clean up if we already have something loaded: if (!unloadTileset()) { //Could not clean up... return false; } //Save the name of the file: fileName = name; //Load the image from the file: framesetImage = ImageLoader.getInstance().loadLocalImage(name); if (framesetImage == null) { System.err.println("TileSet.loadTileset - could not load image."); System.err.flush(); return false; } //Get image info: tilesetRaster = framesetImage.getRaster(); tilesetSpace = framesetImage.getColorModel().getColorSpace(); //Get image dimensions: width = tilesetRaster.getWidth(); height = tilesetRaster.getHeight(); //Define color control variables: transparentColor = ImageUtilities.getInstance() .getPixel(framesetImage, width - 1, 0); boundColor = ImageUtilities.getInstance() .getPixel(framesetImage, width - 1, 1); anchorColor = ImageUtilities.getInstance() .getPixel(framesetImage, width - 1, 2); insideColor = ImageUtilities.getInstance() .getPixel(framesetImage, width - 1, 3); insideAnchorColor = ImageUtilities.getInstance() .getPixel(framesetImage, width - 1, 4); testColor = Color.black; //Find the coordinates of the vertical tile bounds: for (x = 0; x < width; x++) { testColor = ImageUtilities.getInstance().getPixel(framesetImage, x, 0); if (testColor.equals(transparentColor)) { //Add the coordinate to the list: if (VERBOSE) { System.out.println("TileSet: Adding x coordinate - " + x); System.out.flush(); } verticalTileBounds.add(new Integer(x)); } } //Find the coordinates of the horizontal tile bounds: for (y = 0; y < height; y++) { testColor = ImageUtilities.getInstance().getPixel(framesetImage, 0, y); if (testColor.equals(transparentColor)) { //Add the coordinate to the list: if (VERBOSE) { System.out.println("TileSet: Adding y coordinate - " + y); System.out.flush(); } horizontalTileBounds.add(new Integer(y)); } } //Allocate the list of frame infos: frameList = new FrameInformation[(verticalTileBounds.size() - 1) * (horizontalTileBounds.size() - 1)]; //Scan the tiles in: for (row = 0; row < horizontalTileBounds.size() - 1; row++) { for (col = 0; col < verticalTileBounds.size() - 1; col++) { currentTile = col + row * (verticalTileBounds.size() - 1); if (DEBUG) { System.out.println("TileSet: Processing tile (" + col + ", " + row + ") - #" + currentTile); System.out.flush(); } frameList[currentTile] = new FrameInformation(); //Determine the anchor point: frameList[currentTile].getAnchor().x = findFirstOccurrence(((Integer) verticalTileBounds.get(col)) .intValue(), ((Integer) verticalTileBounds.get(col + 1)) .intValue(), ((Integer) horizontalTileBounds.get(row)) .intValue(), anchorColor, insideAnchorColor, false); frameList[currentTile].getAnchor().y = findFirstOccurrence(((Integer) horizontalTileBounds.get(row)) .intValue(), ((Integer) horizontalTileBounds.get(row + 1)) .intValue(), ((Integer) verticalTileBounds.get(col)) .intValue(), anchorColor, insideAnchorColor, true); //Find the first tile pixel: frameList[currentTile].getSource().x = findFirstOccurrence(((Integer) verticalTileBounds.get(col)) .intValue(), ((Integer) verticalTileBounds.get(col + 1)) .intValue(), ((Integer) horizontalTileBounds.get(row)) .intValue(), insideColor, insideAnchorColor, false); frameList[currentTile].getSource().y = findFirstOccurrence(((Integer) horizontalTileBounds.get(row)) .intValue(), ((Integer) horizontalTileBounds.get(row + 1)) .intValue(), ((Integer) verticalTileBounds.get(col)) .intValue(), insideColor, insideAnchorColor, true); if (frameList[currentTile].getSource().x == 0) { //Could not find. Use default: frameList[currentTile].getSource().x = ((Integer) verticalTileBounds.get(col)).intValue() + 1; } if (frameList[currentTile].getSource().y == 0) { //Could not find. Use default: frameList[currentTile].getSource().y = ((Integer) horizontalTileBounds.get(row)).intValue() + 1; } //Find the last tile pixel: frameList[currentTile].getSource().width = findLastOccurrence(((Integer) verticalTileBounds.get(col)) .intValue(), ((Integer) verticalTileBounds.get(col + 1)) .intValue(), ((Integer) horizontalTileBounds.get(row)) .intValue(), insideColor, insideAnchorColor, false) - frameList[currentTile].getSource().x + 1; frameList[currentTile].getSource().height = findLastOccurrence(((Integer) horizontalTileBounds.get(row)) .intValue(), ((Integer) horizontalTileBounds.get(row + 1)) .intValue(), ((Integer) verticalTileBounds.get(col)) .intValue(), insideColor, insideAnchorColor, true) - frameList[currentTile].getSource().y + 1; if (frameList[currentTile].getSource().width <= 0) { //Could not find. Use default: frameList[currentTile].getSource().width = ((Integer) verticalTileBounds.get(col + 1)).intValue() - frameList[currentTile].getSource().x; } if (frameList[currentTile].getSource().height <= 0) { //Could not find. Use default: frameList[currentTile].getSource().height = ((Integer) horizontalTileBounds.get(row + 1)).intValue() - frameList[currentTile].getSource().y; } //Calculate tile extent: frameList[currentTile] .setExtent(new Rectangle(frameList[currentTile].getSource())); frameList[currentTile].getExtent() .translate(-frameList[currentTile].getAnchor().x, -frameList[currentTile].getAnchor().y); } } specialLoad(); return true; } /** * Describe specialLoad method here. * * @return a boolean value */ protected boolean specialLoad() { return true; } /** * Describe getFrameList method here. * * @return a FrameInformation[] value */ protected FrameInformation[] getFrameList() { return frameList; } /** * Describe getFramesetImage method here. * * @return a BufferedImage value */ protected BufferedImage getFramesetImage() { return framesetImage; } /** * Finds the coordinate of the first occurrence of one of the test colors. * * @param lowBound an int value * @param highBound an int value * @param coordinate an int value * @param c1 a Color value * @param c2 a Color value * @param vertical a boolean value * @return an int value */ protected int findFirstOccurrence(int lowBound, int highBound, int coordinate, Color c1, Color c2, boolean vertical) { Color testColor = null; int x = 0; int y = 0; for (int i = lowBound + 1; i < highBound; i++) { if (vertical) { x = coordinate; y = i; } else { x = i; y = coordinate; } testColor = ImageUtilities.getInstance().getPixel(framesetImage, x, y); if (testColor.equals(c1) || testColor.equals(c2)) { return i; } } return 0; } /** * Finds the coordinate of the last occurrence of one of the test colors. * * @param lowBound an int value * @param highBound an int value * @param coordinate an int value * @param c1 a Color value * @param c2 a Color value * @param vertical a boolean value * @return an int value */ protected int findLastOccurrence(int lowBound, int highBound, int coordinate, Color c1, Color c2, boolean vertical) { Color testColor = null; int x = 0; int y = 0; for (int i = highBound - 1; i > lowBound; i--) { if (vertical) { x = coordinate; y = i; } else { x = i; y = coordinate; } testColor = ImageUtilities.getInstance().getPixel(framesetImage, x, y); if (testColor.equals(c1) || testColor.equals(c2)) { return i; } } return 0; } /** * If the surface was lost, reloads it, but does not reparse the file. * * @return a boolean value */ public boolean reloadTileset() { //Load the image from the file: framesetImage = ImageLoader.getInstance().loadLocalImage(getFileName()); if (framesetImage == null) { System.err.println("TileSet.reloadTileset - could not load image"); System.err.flush(); return false; } return true; } /** * Performs all the necessary clean-up operations. * * @return a boolean value */ public boolean unloadTileset() { //Clean up the tile list if necessary: if (frameList != null) { frameList = null; fileName = null; framesetImage = null; } return true; } /** * Returns the number of tiles in the tileset. * @return an int value */ public int getTileCount() { return frameList.length; } /** * Returns the image on which the tileset resides. * @return a BufferedImage value */ public BufferedImage getImage() { return framesetImage; } /** * Returns the name of the file containing the tileset in a string. * @return a String value */ public String getFileName() { return fileName; } /** * Returns the dimensions of the largest tile in the tileset. * * @return a Dimension value */ public Dimension getLargestDimension() { Dimension result = new Dimension(); for (int i = 0; i < frameList.length; i++) { int width = frameList[i].getSource().width; int height = frameList[i].getSource().height; if (width > result.width) { result.width = width; } if (height > result.height) { result.height = height; } } return result; } /** * Returns the dimensions of the tile with the specified id. * * @param tileNum an int value * @return a Dimension value */ public Dimension getDimension(int tileNum) { Dimension result = new Dimension(); result.width = frameList[tileNum].getSource().width; result.height = frameList[tileNum].getSource().height; return result; } /** * Returns the extent of the tile with the specified id. * * @param tileNum an int value * @return a Rectangle value */ public Rectangle getExtent(int tileNum) { Rectangle result = new Rectangle(frameList[tileNum].getExtent()); return result; } /** * Puts the tile with the specified number onto the graphics context * passed in at the specified coordinates. Note that the position of * the tile will depend on its anchor point, which will be put * exactly at the coordinates passed in. * * @param destination the context to which we want to draw the tile. * @param coordinates the coordinates at which we want to draw the tile. * @param tileNum the number of the tile we want to draw. The tiles * will be numbered automatically, from left to right, from top to * bottom, when the image file is parsed. */ public void drawTile(Graphics destination, Point coordinates, int tileNum) { if (DEBUG) { System.out.println("TileSet.drawTile - Drawing tile " + tileNum + " at (" + coordinates.x + ", " + coordinates.y + ")"); System.out.flush(); } frameList[tileNum].getExtent().translate(coordinates.x, coordinates.y); //Fetch raster: Rectangle src = frameList[tileNum].getSource(); if (VERBOSE) { System.out.println("TileSet.drawTile: src rectangle - " + src); System.out.flush(); } Raster data = framesetImage.getData(src); if (VERBOSE) { System.out.println("TileSet.drawTile: data - " + data); System.out.flush(); } BufferedImage temp = new BufferedImage(data.getWidth(), data.getHeight(), framesetImage.getType()); temp.setData(data.createRaster(data.getSampleModel(), data.getDataBuffer(), null)); if (VERBOSE) { System.out.println("TileSet.drawTile: temp - " + temp); System.out.flush(); } //Render tile: destination.drawImage(temp, frameList[tileNum].getExtent().x, frameList[tileNum].getExtent().y, null); frameList[tileNum].getExtent().translate(-coordinates.x, -coordinates.y); } /** * Information structure for one tile in the tileset. */ protected class FrameInformation { /** * This rectangle represents the location of the frame in the * master frame set. */ private Rectangle source; /** * The extent of the frame. */ private Rectangle extent; /** * The anchor point of the frame. That is, when we say that we * want to render the frame at certain coordinates, the point with * the coordinates stored here, relative to the top-left corner of * the frame, will be drawn at the specified coordinates. */ private Point anchor; /** * Creates a new FrameInformation * instance. Initializes all of the data members to contain all * default values (zeros). */ public FrameInformation() { source = new Rectangle(); extent = new Rectangle(); anchor = new Point(); } /** * Get the value of source. * @return value of source. */ public Rectangle getSource() { return source; } /** * Set the value of source. * @param v Value to assign to source. */ public void setSource(Rectangle v) { this.source = v; } /** * Get the value of extent. * @return value of extent. */ public Rectangle getExtent() { return extent; } /** * Set the value of extent. * @param v Value to assign to extent. */ public void setExtent(Rectangle v) { this.extent = v; } /** * Get the value of anchor. * @return value of anchor. */ public Point getAnchor() { return anchor; } /** * Set the value of anchor. * @param v Value to assign to anchor. */ public void setAnchor(Point v) { this.anchor = v; } } }