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