/* * Tiled Map Editor, (c) 2004-2006 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Adam Turk <aturk@biggeruniverse.com> * Bjorn Lindeijer <bjorn@lindeijer.nl> */ package tiled.core; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Area; import java.util.HashMap; import java.util.Properties; /** * A TileLayer is a specialized MapLayer, used for tracking two dimensional * tile data. * * @version $Id$ */ public class TileLayer extends MapLayer { protected Tile[][] map; protected HashMap<Object, Properties> tileInstanceProperties = new HashMap<Object, Properties>(); public Properties getTileInstancePropertiesAt(int x, int y) { if (!bounds.contains(x, y)) { return null; } Object key = new Point(x, y); return tileInstanceProperties.get(key); } public void setTileInstancePropertiesAt(int x, int y, Properties tip) { if (bounds.contains(x, y)) { Object key = new Point(x, y); tileInstanceProperties.put(key, tip); } } /** * Default contructor. */ public TileLayer() { } /** * Construct a TileLayer from the given width and height. * * @param w width in tiles * @param h height in tiles */ public TileLayer(int w, int h) { super(w, h); } /** * Create a tile layer using the given bounds. * * @param r the bounds of the tile layer. */ public TileLayer(Rectangle r) { super(r); } /** * @param m the map this layer is part of */ TileLayer(Map m) { super(m); } /** * @param m the map this layer is part of * @param w width in tiles * @param h height in tiles */ public TileLayer(Map m, int w, int h) { super(w, h); setMap(m); } /** * Rotates the layer by the given Euler angle. * * @param angle The Euler angle (0-360) to rotate the layer array data by. * @see MapLayer#rotate(int) */ @Override public void rotate(int angle) { Tile[][] trans; int xtrans = 0, ytrans = 0; if (!canEdit()) return; switch (angle) { case ROTATE_90: trans = new Tile[bounds.width][bounds.height]; xtrans = bounds.height - 1; break; case ROTATE_180: trans = new Tile[bounds.height][bounds.width]; xtrans = bounds.width - 1; ytrans = bounds.height - 1; break; case ROTATE_270: trans = new Tile[bounds.width][bounds.height]; ytrans = bounds.width - 1; break; default: System.out.println("Unsupported rotation (" + angle + ")"); return; } double ra = Math.toRadians(angle); int cos_angle = (int)Math.round(Math.cos(ra)); int sin_angle = (int)Math.round(Math.sin(ra)); for (int y = 0; y < bounds.height; y++) { for (int x = 0; x < bounds.width; x++) { int xrot = x * cos_angle - y * sin_angle; int yrot = x * sin_angle + y * cos_angle; trans[yrot + ytrans][xrot + xtrans] = getTileAt(x+bounds.x, y+bounds.y); } } bounds.width = trans[0].length; bounds.height = trans.length; map = trans; } /** * Performs a mirroring function on the layer data. Two orientations are * allowed: vertical and horizontal. * * Example: <code>layer.mirror(MapLayer.MIRROR_VERTICAL);</code> will * mirror the layer data around a horizontal axis. * * @param dir the axial orientation to mirror around */ @Override public void mirror(int dir) { if (!canEdit()) return; Tile[][] mirror = new Tile[bounds.height][bounds.width]; for (int y = 0; y < bounds.height; y++) { for (int x = 0; x < bounds.width; x++) { if (dir == MIRROR_VERTICAL) { mirror[y][x] = map[bounds.height - 1 - y][x]; } else { mirror[y][x] = map[y][bounds.width - 1 - x]; } } } map = mirror; } /** * Checks to see if the given Tile is used anywhere in the layer. * * @param t a Tile object to check for * @return <code>true</code> if the Tile is used at least once, * <code>false</code> otherwise. */ public boolean isUsed(Tile t) { for (int y = 0; y < bounds.height; y++) { for (int x = 0; x < bounds.width; x++) { if (map[y][x] == t) { return true; } } } return false; } @Override public boolean isEmpty() { for (int p = 0; p < 2; p++) { for (int y = 0; y < bounds.height; y++) { for (int x = p; x < bounds.width; x += 2) { if (map[y][x] != null) return false; } } } return true; } /** * Sets the bounds (in tiles) to the specified Rectangle. <b>Caution:</b> * this causes a reallocation of the data array, and all previous data is * lost. * * @param bounds new new bounds of this tile layer (in tiles) * @see MapLayer#setBounds */ @Override protected void setBounds(Rectangle bounds) { super.setBounds(bounds); map = new Tile[bounds.height][bounds.width]; // Tile instance properties is null when this method is called from // the constructor of MapLayer if (tileInstanceProperties != null) { tileInstanceProperties.clear(); } } /** * Creates a diff of the two layers, <code>ml</code> is considered the * significant difference. * * @param ml * @return A new MapLayer that represents the difference between this * layer, and the argument, or <b>null</b> if no difference exists. */ @Override public MapLayer createDiff(MapLayer ml) { if (ml == null) { return null; } if (ml instanceof TileLayer) { Rectangle r = null; for (int y = bounds.y; y < bounds.height + bounds.y; y++) { for (int x = bounds.x; x < bounds.width + bounds.x; x++) { if (((TileLayer)ml).getTileAt(x, y) != getTileAt(x, y)) { if (r != null) { r.add(x, y); } else { r = new Rectangle(new Point(x, y)); } } } } if (r != null) { MapLayer diff = new TileLayer( new Rectangle(r.x, r.y, r.width + 1, r.height + 1)); diff.copyFrom(ml); return diff; } else { return new TileLayer(); } } else { return null; } } /** * Removes any occurences of the given tile from this map layer. If layer * is locked, an exception is thrown. * * @param tile the Tile to be removed * @throws LayerLockedException when this layer is locked */ public void removeTile(Tile tile) throws LayerLockedException { if (getLocked()) { throw new LayerLockedException( "Attempted to remove tile when this layer is locked."); } for (int y = 0; y < bounds.height; y++) { for (int x = 0; x < bounds.width; x++) { if (map[y][x] == tile) { setTileAt(x + bounds.x, y + bounds.y, null); } } } } /** * Sets the tile at the specified position. Does nothing if (tx, ty) falls * outside of this layer. * * @param tx x position of tile * @param ty y position of tile * @param ti the tile object to place */ public void setTileAt(int tx, int ty, Tile ti) { if (bounds.contains(tx, ty) && canEdit()) { map[ty - bounds.y][tx - bounds.x] = ti; } } /** * Returns the tile at the specified position. * * @param tx Tile-space x coordinate * @param ty Tile-space y coordinate * @return tile at position (tx, ty) or <code>null</code> when (tx, ty) is * outside this layer */ public Tile getTileAt(int tx, int ty) { return (bounds.contains(tx, ty)) ? map[ty - bounds.y][tx - bounds.x] : null; } /** * Returns the first occurance (using top down, left to right search) of * the given tile. * * @param t the {@link Tile} to look for * @return A java.awt.Point instance of the first instance of t, or * <code>null</code> if it is not found */ public Point locationOf(Tile t) { for (int y = bounds.y; y < bounds.height + bounds.y; y++) { for (int x = bounds.x; x < bounds.width + bounds.x; x++) { if (getTileAt(x, y) == t) { return new Point(x, y); } } } return null; } /** * Replaces all occurances of the Tile <code>find</code> with the Tile * <code>replace</code> in the entire layer * * @param find the tile to replace * @param replace the replacement tile */ public void replaceTile(Tile find, Tile replace) { if (!canEdit()) return; for (int y = bounds.y; y < bounds.y + bounds.height; y++) { for (int x = bounds.x; x < bounds.x + bounds.width; x++) { if(getTileAt(x,y) == find) { setTileAt(x, y, replace); } } } } /** * @inheritDoc MapLayer#mergeOnto(MapLayer) */ @Override public void mergeOnto(MapLayer other) { if (!other.canEdit()) return; for (int y = bounds.y; y < bounds.y + bounds.height; y++) { for (int x = bounds.x; x < bounds.x + bounds.width; x++) { Tile tile = getTileAt(x, y); if (tile != null) { ((TileLayer) other).setTileAt(x, y, tile); } } } } /** * Like mergeOnto, but will only copy the area specified. * * @see TileLayer#mergeOnto(MapLayer) * @param other * @param mask */ @Override public void maskedMergeOnto(MapLayer other, Area mask) { if (!canEdit()) return; Rectangle boundBox = mask.getBounds(); for (int y = boundBox.y; y < boundBox.y + boundBox.height; y++) { for (int x = boundBox.x; x < boundBox.x + boundBox.width; x++) { Tile tile = ((TileLayer) other).getTileAt(x, y); if (mask.contains(x, y) && tile != null) { setTileAt(x, y, tile); } } } } /** * Copy data from another layer onto this layer. Unlike mergeOnto, * copyFrom() copies the empty cells as well. * * @see MapLayer#mergeOnto * @param other */ @Override public void copyFrom(MapLayer other) { if (!canEdit()) return; for (int y = bounds.y; y < bounds.y + bounds.height; y++) { for (int x = bounds.x; x < bounds.x + bounds.width; x++) { setTileAt(x, y, ((TileLayer) other).getTileAt(x, y)); } } } /** * Like copyFrom, but will only copy the area specified. * * @see TileLayer#copyFrom(MapLayer) * @param other * @param mask */ @Override public void maskedCopyFrom(MapLayer other, Area mask) { if (!canEdit()) return; Rectangle boundBox = mask.getBounds(); for (int y = boundBox.y; y < boundBox.y + boundBox.height; y++) { for (int x = boundBox.x; x < boundBox.x + boundBox.width; x++) { if (mask.contains(x,y)) { setTileAt(x, y, ((TileLayer) other).getTileAt(x, y)); } } } } /** * Unlike mergeOnto, copyTo includes the null tile when merging. * * @see MapLayer#copyFrom * @see MapLayer#mergeOnto * @param other the layer to copy this layer to */ @Override public void copyTo(MapLayer other) { if (!other.canEdit()) return; for (int y = bounds.y; y < bounds.y + bounds.height; y++) { for (int x = bounds.x; x < bounds.x + bounds.width; x++) { ((TileLayer) other).setTileAt(x, y, getTileAt(x, y)); } } } /** * Creates a copy of this layer. * * @see Object#clone * @return a clone of this layer, as complete as possible * @exception CloneNotSupportedException */ @Override public Object clone() throws CloneNotSupportedException { TileLayer clone = (TileLayer) super.clone(); // Clone the layer data clone.map = new Tile[map.length][]; clone.tileInstanceProperties = new HashMap<Object, Properties>(); for (int i = 0; i < map.length; i++) { clone.map[i] = new Tile[map[i].length]; System.arraycopy(map[i], 0, clone.map[i], 0, map[i].length); for (int j = 0; j < map[i].length; j++) { Properties p = getTileInstancePropertiesAt(i, j); if (p != null) { Integer key = i + j * bounds.width; clone.tileInstanceProperties.put(key, (Properties) p.clone()); } } } return clone; } /** * @see MultilayerPlane#resize * * @param width the new width of the layer * @param height the new height of the layer * @param dx the shift in x direction * @param dy the shift in y direction */ @Override public void resize(int width, int height, int dx, int dy) { if (!canEdit()) return; Tile[][] newMap = new Tile[height][width]; HashMap<Object, Properties> newTileInstanceProperties = new HashMap<Object, Properties>(); int maxX = Math.min(width, bounds.width + dx); int maxY = Math.min(height, bounds.height + dy); for (int x = Math.max(0, dx); x < maxX; x++) { for (int y = Math.max(0, dy); y < maxY; y++) { newMap[y][x] = getTileAt(x - dx, y - dy); Properties tip = getTileInstancePropertiesAt(x - dx, y - dy); if (tip != null) { newTileInstanceProperties.put(new Point(x, y), tip); } } } map = newMap; tileInstanceProperties = newTileInstanceProperties; bounds.width = width; bounds.height = height; } }