/*
* Room.java 18 nov. 2008
*
* Sweet Home 3D, Copyright (c) 2008 Emmanuel PUYBARET / eTeks <info@eteks.com>
*
* 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.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.eteks.sweethome3d.model;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A room or a polygon in a home plan.
* @author Emmanuel Puybaret
*/
public class Room implements Serializable, Selectable, Elevatable {
/**
* The properties of a room that may change. <code>PropertyChangeListener</code>s added
* to a room will be notified under a property name equal to the string value of one these properties.
*/
public enum Property {NAME, NAME_X_OFFSET, NAME_Y_OFFSET, NAME_STYLE, NAME_ANGLE,
POINTS, AREA_VISIBLE, AREA_X_OFFSET, AREA_Y_OFFSET, AREA_STYLE, AREA_ANGLE,
FLOOR_COLOR, FLOOR_TEXTURE, FLOOR_VISIBLE, FLOOR_SHININESS,
CEILING_COLOR, CEILING_TEXTURE, CEILING_VISIBLE, CEILING_SHININESS, LEVEL}
private static final long serialVersionUID = 1L;
private static final double TWICE_PI = 2 * Math.PI;
private String name;
private float nameXOffset;
private float nameYOffset;
private TextStyle nameStyle;
private float nameAngle;
private float [][] points;
private boolean areaVisible;
private float areaXOffset;
private float areaYOffset;
private TextStyle areaStyle;
private float areaAngle;
private boolean floorVisible;
private Integer floorColor;
private HomeTexture floorTexture;
private float floorShininess;
private boolean ceilingVisible;
private Integer ceilingColor;
private HomeTexture ceilingTexture;
private float ceilingShininess;
private Level level;
private transient PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
private transient Shape shapeCache;
private transient Float areaCache;
/**
* Creates a room from its name and the given coordinates.
*/
public Room(float [][] points) {
if (points.length <= 1) {
throw new IllegalStateException("Room points must containt at least two points");
}
this.points = deepCopy(points);
this.areaVisible = true;
this.nameYOffset = -40f;
this.floorVisible = true;
this.ceilingVisible = true;
}
/**
* Initializes new room transient fields
* and reads room from <code>in</code> stream with default reading method.
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
this.propertyChangeSupport = new PropertyChangeSupport(this);
in.defaultReadObject();
}
/**
* Adds the property change <code>listener</code> in parameter to this wall.
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
this.propertyChangeSupport.addPropertyChangeListener(listener);
}
/**
* Removes the property change <code>listener</code> in parameter from this wall.
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
this.propertyChangeSupport.removePropertyChangeListener(listener);
}
/**
* Returns the name of this room.
*/
public String getName() {
return this.name;
}
/**
* Sets the name of this room. Once this room is updated,
* listeners added to this room will receive a change notification.
*/
public void setName(String name) {
if (name != this.name
&& (name == null || !name.equals(this.name))) {
String oldName = this.name;
this.name = name;
this.propertyChangeSupport.firePropertyChange(Property.NAME.name(), oldName, name);
}
}
/**
* Returns the distance along x axis applied to room center abscissa
* to display room name.
*/
public float getNameXOffset() {
return this.nameXOffset;
}
/**
* Sets the distance along x axis applied to room center abscissa to display room name.
* Once this room is updated, listeners added to this room will receive a change notification.
*/
public void setNameXOffset(float nameXOffset) {
if (nameXOffset != this.nameXOffset) {
float oldNameXOffset = this.nameXOffset;
this.nameXOffset = nameXOffset;
this.propertyChangeSupport.firePropertyChange(Property.NAME_X_OFFSET.name(), oldNameXOffset, nameXOffset);
}
}
/**
* Returns the distance along y axis applied to room center ordinate
* to display room name.
*/
public float getNameYOffset() {
return this.nameYOffset;
}
/**
* Sets the distance along y axis applied to room center ordinate to display room name.
* Once this room is updated, listeners added to this room will receive a change notification.
*/
public void setNameYOffset(float nameYOffset) {
if (nameYOffset != this.nameYOffset) {
float oldNameYOffset = this.nameYOffset;
this.nameYOffset = nameYOffset;
this.propertyChangeSupport.firePropertyChange(Property.NAME_Y_OFFSET.name(), oldNameYOffset, nameYOffset);
}
}
/**
* Returns the text style used to display room name.
*/
public TextStyle getNameStyle() {
return this.nameStyle;
}
/**
* Sets the text style used to display room name.
* Once this room is updated, listeners added to this room will receive a change notification.
*/
public void setNameStyle(TextStyle nameStyle) {
if (nameStyle != this.nameStyle) {
TextStyle oldNameStyle = this.nameStyle;
this.nameStyle = nameStyle;
this.propertyChangeSupport.firePropertyChange(Property.NAME_STYLE.name(), oldNameStyle, nameStyle);
}
}
/**
* Returns the angle in radians used to display the room name.
* @since 3.6
*/
public float getNameAngle() {
return this.nameAngle;
}
/**
* Sets the angle in radians used to display the room name. Once this piece is updated,
* listeners added to this piece will receive a change notification.
* @since 3.6
*/
public void setNameAngle(float nameAngle) {
// Ensure angle is always positive and between 0 and 2 PI
nameAngle = (float)((nameAngle % TWICE_PI + TWICE_PI) % TWICE_PI);
if (nameAngle != this.nameAngle) {
float oldNameAngle = this.nameAngle;
this.nameAngle = nameAngle;
this.propertyChangeSupport.firePropertyChange(Property.NAME_ANGLE.name(), oldNameAngle, nameAngle);
}
}
/**
* Returns the points of the polygon matching this room.
* @return an array of the (x,y) coordinates of the room points.
*/
public float [][] getPoints() {
return deepCopy(this.points);
}
/**
* Returns the number of points of the polygon matching this room.
* @since 2.0
*/
public int getPointCount() {
return this.points.length;
}
private float [][] deepCopy(float [][] points) {
float [][] pointsCopy = new float [points.length][];
for (int i = 0; i < points.length; i++) {
pointsCopy [i] = points [i].clone();
}
return pointsCopy;
}
/**
* Sets the points of the polygon matching this room. Once this room
* is updated, listeners added to this room will receive a change notification.
*/
public void setPoints(float [][] points) {
if (!Arrays.deepEquals(this.points, points)) {
updatePoints(points);
}
}
/**
* Update the points of the polygon matching this room.
*/
private void updatePoints(float [][] points) {
float [][] oldPoints = this.points;
this.points = deepCopy(points);
this.shapeCache = null;
this.areaCache = null;
this.propertyChangeSupport.firePropertyChange(Property.POINTS.name(), oldPoints, points);
}
/**
* Adds a point at the end of room points.
* @since 2.0
*/
public void addPoint(float x, float y) {
addPoint(x, y, this.points.length);
}
/**
* Adds a point at the given <code>index</code>.
* @throws IndexOutOfBoundsException if <code>index</code> is negative or > <code>getPointCount()</code>
* @since 2.0
*/
public void addPoint(float x, float y, int index) {
if (index < 0 || index > this.points.length) {
throw new IndexOutOfBoundsException("Invalid index " + index);
}
float [][] newPoints = new float [this.points.length + 1][];
System.arraycopy(this.points, 0, newPoints, 0, index);
newPoints [index] = new float [] {x, y};
System.arraycopy(this.points, index, newPoints, index + 1, this.points.length - index);
float [][] oldPoints = this.points;
this.points = newPoints;
this.shapeCache = null;
this.areaCache = null;
this.propertyChangeSupport.firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
}
/**
* Sets the point at the given <code>index</code>.
* @throws IndexOutOfBoundsException if <code>index</code> is negative or >= <code>getPointCount()</code>
* @since 2.0
*/
public void setPoint(float x, float y, int index) {
if (index < 0 || index >= this.points.length) {
throw new IndexOutOfBoundsException("Invalid index " + index);
}
if (this.points [index][0] != x
|| this.points [index][1] != y) {
float [][] oldPoints = this.points;
this.points = deepCopy(this.points);
this.points [index][0] = x;
this.points [index][1] = y;
this.shapeCache = null;
this.areaCache = null;
this.propertyChangeSupport.firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
}
}
/**
* Removes the point at the given <code>index</code>.
* @throws IndexOutOfBoundsException if <code>index</code> is negative or >= <code>getPointCount()</code>
* @since 2.0
*/
public void removePoint(int index) {
if (index < 0 || index >= this.points.length) {
throw new IndexOutOfBoundsException("Invalid index " + index);
} else if (this.points.length <= 1) {
throw new IllegalStateException("Room points must containt at least one point");
}
float [][] newPoints = new float [this.points.length - 1][];
System.arraycopy(this.points, 0, newPoints, 0, index);
System.arraycopy(this.points, index + 1, newPoints, index, this.points.length - index - 1);
float [][] oldPoints = this.points;
this.points = newPoints;
this.shapeCache = null;
this.areaCache = null;
this.propertyChangeSupport.firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
}
/**
* Returns whether the area of this room is visible or not.
*/
public boolean isAreaVisible() {
return this.areaVisible;
}
/**
* Sets whether the area of this room is visible or not. Once this room
* is updated, listeners added to this room will receive a change notification.
*/
public void setAreaVisible(boolean areaVisible) {
if (areaVisible != this.areaVisible) {
this.areaVisible = areaVisible;
this.propertyChangeSupport.firePropertyChange(Property.AREA_VISIBLE.name(), !areaVisible, areaVisible);
}
}
/**
* Returns the distance along x axis applied to room center abscissa
* to display room area.
*/
public float getAreaXOffset() {
return this.areaXOffset;
}
/**
* Sets the distance along x axis applied to room center abscissa to display room area.
* Once this room is updated, listeners added to this room will receive a change notification.
*/
public void setAreaXOffset(float areaXOffset) {
if (areaXOffset != this.areaXOffset) {
float oldAreaXOffset = this.areaXOffset;
this.areaXOffset = areaXOffset;
this.propertyChangeSupport.firePropertyChange(Property.AREA_X_OFFSET.name(), oldAreaXOffset, areaXOffset);
}
}
/**
* Returns the distance along y axis applied to room center ordinate
* to display room area.
*/
public float getAreaYOffset() {
return this.areaYOffset;
}
/**
* Sets the distance along y axis applied to room center ordinate to display room area.
* Once this room is updated, listeners added to this room will receive a change notification.
*/
public void setAreaYOffset(float areaYOffset) {
if (areaYOffset != this.areaYOffset) {
float oldAreaYOffset = this.areaYOffset;
this.areaYOffset = areaYOffset;
this.propertyChangeSupport.firePropertyChange(Property.AREA_Y_OFFSET.name(), oldAreaYOffset, areaYOffset);
}
}
/**
* Returns the text style used to display room area.
*/
public TextStyle getAreaStyle() {
return this.areaStyle;
}
/**
* Sets the text style used to display room area.
* Once this room is updated, listeners added to this room will receive a change notification.
*/
public void setAreaStyle(TextStyle areaStyle) {
if (areaStyle != this.areaStyle) {
TextStyle oldAreaStyle = this.areaStyle;
this.areaStyle = areaStyle;
this.propertyChangeSupport.firePropertyChange(Property.AREA_STYLE.name(), oldAreaStyle, areaStyle);
}
}
/**
* Returns the angle in radians used to display the room area.
* @since 3.6
*/
public float getAreaAngle() {
return this.areaAngle;
}
/**
* Sets the angle in radians used to display the room area. Once this piece is updated,
* listeners added to this piece will receive a change notification.
* @since 3.6
*/
public void setAreaAngle(float areaAngle) {
// Ensure angle is always positive and between 0 and 2 PI
areaAngle = (float)((areaAngle % TWICE_PI + TWICE_PI) % TWICE_PI);
if (areaAngle != this.areaAngle) {
float oldAreaAngle = this.areaAngle;
this.areaAngle = areaAngle;
this.propertyChangeSupport.firePropertyChange(Property.AREA_ANGLE.name(), oldAreaAngle, areaAngle);
}
}
/**
* Returns the abscissa of the center point of this room.
*/
public float getXCenter() {
float xMin = this.points [0][0];
float xMax = this.points [0][0];
for (int i = 1; i < this.points.length; i++) {
xMin = Math.min(xMin, this.points [i][0]);
xMax = Math.max(xMax, this.points [i][0]);
}
return (xMin + xMax) / 2;
}
/**
* Returns the ordinate of the center point of this room.
*/
public float getYCenter() {
float yMin = this.points [0][1];
float yMax = this.points [0][1];
for (int i = 1; i < this.points.length; i++) {
yMin = Math.min(yMin, this.points [i][1]);
yMax = Math.max(yMax, this.points [i][1]);
}
return (yMin + yMax) / 2;
}
/**
* Returns the floor color of this room.
*/
public Integer getFloorColor() {
return this.floorColor;
}
/**
* Sets the floor color of this room. Once this room is updated,
* listeners added to this room will receive a change notification.
*/
public void setFloorColor(Integer floorColor) {
if (floorColor != this.floorColor
&& (floorColor == null || !floorColor.equals(this.floorColor))) {
Integer oldFloorColor = this.floorColor;
this.floorColor = floorColor;
this.propertyChangeSupport.firePropertyChange(Property.FLOOR_COLOR.name(),
oldFloorColor, floorColor);
}
}
/**
* Returns the floor texture of this room.
*/
public HomeTexture getFloorTexture() {
return this.floorTexture;
}
/**
* Sets the floor texture of this room. Once this room is updated,
* listeners added to this room will receive a change notification.
*/
public void setFloorTexture(HomeTexture floorTexture) {
if (floorTexture != this.floorTexture
&& (floorTexture == null || !floorTexture.equals(this.floorTexture))) {
HomeTexture oldFloorTexture = this.floorTexture;
this.floorTexture = floorTexture;
this.propertyChangeSupport.firePropertyChange(Property.FLOOR_TEXTURE.name(),
oldFloorTexture, floorTexture);
}
}
/**
* Returns whether the floor of this room is visible or not.
*/
public boolean isFloorVisible() {
return this.floorVisible;
}
/**
* Sets whether the floor of this room is visible or not. Once this room
* is updated, listeners added to this room will receive a change notification.
*/
public void setFloorVisible(boolean floorVisible) {
if (floorVisible != this.floorVisible) {
this.floorVisible = floorVisible;
this.propertyChangeSupport.firePropertyChange(Property.FLOOR_VISIBLE.name(), !floorVisible, floorVisible);
}
}
/**
* Returns the floor shininess of this room.
* @return a value between 0 (matt) and 1 (very shiny)
* @since 3.0
*/
public float getFloorShininess() {
return this.floorShininess;
}
/**
* Sets the floor shininess of this room. Once this room is updated,
* listeners added to this room will receive a change notification.
* @since 3.0
*/
public void setFloorShininess(float floorShininess) {
if (floorShininess != this.floorShininess) {
float oldFloorShininess = this.floorShininess;
this.floorShininess = floorShininess;
this.propertyChangeSupport.firePropertyChange(Property.FLOOR_SHININESS.name(),
oldFloorShininess, floorShininess);
}
}
/**
* Returns the ceiling color color of this room.
*/
public Integer getCeilingColor() {
return this.ceilingColor;
}
/**
* Sets the ceiling color of this room. Once this room is updated,
* listeners added to this room will receive a change notification.
*/
public void setCeilingColor(Integer ceilingColor) {
if (ceilingColor != this.ceilingColor
&& (ceilingColor == null || !ceilingColor.equals(this.ceilingColor))) {
Integer oldCeilingColor = this.ceilingColor;
this.ceilingColor = ceilingColor;
this.propertyChangeSupport.firePropertyChange(Property.CEILING_COLOR.name(),
oldCeilingColor, ceilingColor);
}
}
/**
* Returns the ceiling texture of this room.
*/
public HomeTexture getCeilingTexture() {
return this.ceilingTexture;
}
/**
* Sets the ceiling texture of this room. Once this room is updated,
* listeners added to this room will receive a change notification.
*/
public void setCeilingTexture(HomeTexture ceilingTexture) {
if (ceilingTexture != this.ceilingTexture
&& (ceilingTexture == null || !ceilingTexture.equals(this.ceilingTexture))) {
HomeTexture oldCeilingTexture = this.ceilingTexture;
this.ceilingTexture = ceilingTexture;
this.propertyChangeSupport.firePropertyChange(Property.CEILING_TEXTURE.name(),
oldCeilingTexture, ceilingTexture);
}
}
/**
* Returns whether the ceiling of this room is visible or not.
*/
public boolean isCeilingVisible() {
return this.ceilingVisible;
}
/**
* Sets whether the ceiling of this room is visible or not. Once this room
* is updated, listeners added to this room will receive a change notification.
*/
public void setCeilingVisible(boolean ceilingVisible) {
if (ceilingVisible != this.ceilingVisible) {
this.ceilingVisible = ceilingVisible;
this.propertyChangeSupport.firePropertyChange(Property.CEILING_VISIBLE.name(), !ceilingVisible, ceilingVisible);
}
}
/**
* Returns the ceiling shininess of this room.
* @return a value between 0 (matt) and 1 (very shiny)
* @since 3.0
*/
public float getCeilingShininess() {
return this.ceilingShininess;
}
/**
* Sets the ceiling shininess of this room. Once this room is updated,
* listeners added to this room will receive a change notification.
* @since 3.0
*/
public void setCeilingShininess(float ceilingShininess) {
if (ceilingShininess != this.ceilingShininess) {
float oldCeilingShininess = this.ceilingShininess;
this.ceilingShininess = ceilingShininess;
this.propertyChangeSupport.firePropertyChange(Property.CEILING_SHININESS.name(),
oldCeilingShininess, ceilingShininess);
}
}
/**
* Returns the level which this room belongs to.
* @since 3.4
*/
public Level getLevel() {
return this.level;
}
/**
* Sets the level of this room. Once this room is updated,
* listeners added to this room will receive a change notification.
* @since 3.4
*/
public void setLevel(Level level) {
if (level != this.level) {
Level oldLevel = this.level;
this.level = level;
this.propertyChangeSupport.firePropertyChange(Property.LEVEL.name(), oldLevel, level);
}
}
/**
* Returns <code>true</code> if this room is at the given level.
* @since 3.4
*/
public boolean isAtLevel(Level level) {
return this.level == level;
}
/**
* Returns the area of this room.
*/
public float getArea() {
if (this.areaCache == null) {
Area roomArea = new Area(getShape());
if (roomArea.isSingular()) {
this.areaCache = Math.abs(getSignedArea(getPoints()));
} else {
// Add the surface of the different polygons of this room
float area = 0;
List<float []> currentPathPoints = new ArrayList<float[]>();
for (PathIterator it = roomArea.getPathIterator(null); !it.isDone(); ) {
float [] roomPoint = new float[2];
switch (it.currentSegment(roomPoint)) {
case PathIterator.SEG_MOVETO :
currentPathPoints.add(roomPoint);
break;
case PathIterator.SEG_LINETO :
currentPathPoints.add(roomPoint);
break;
case PathIterator.SEG_CLOSE :
float [][] pathPoints =
currentPathPoints.toArray(new float [currentPathPoints.size()][]);
area += Math.abs(getSignedArea(pathPoints));
currentPathPoints.clear();
break;
}
it.next();
}
this.areaCache = area;
}
}
return this.areaCache;
}
private float getSignedArea(float areaPoints [][]) {
// From "Area of a General Polygon" algorithm described in
// http://www.davidchandler.com/AreaOfAGeneralPolygon.pdf
float area = 0;
for (int i = 1; i < areaPoints.length; i++) {
area += areaPoints [i][0] * areaPoints [i - 1][1];
area -= areaPoints [i][1] * areaPoints [i - 1][0];
}
area += areaPoints [0][0] * areaPoints [areaPoints.length - 1][1];
area -= areaPoints [0][1] * areaPoints [areaPoints.length - 1][0];
return area / 2;
}
/**
* Returns <code>true</code> if the points of this room are in clockwise order.
*/
public boolean isClockwise() {
return getSignedArea(getPoints()) < 0;
}
/**
* Returns <code>true</code> if this room is comprised of only one polygon.
*/
public boolean isSingular() {
return new Area(getShape()).isSingular();
}
/**
* Returns <code>true</code> if this room intersects
* with the horizontal rectangle which opposite corners are at points
* (<code>x0</code>, <code>y0</code>) and (<code>x1</code>, <code>y1</code>).
*/
public boolean intersectsRectangle(float x0, float y0, float x1, float y1) {
Rectangle2D rectangle = new Rectangle2D.Float(x0, y0, 0, 0);
rectangle.add(x1, y1);
return getShape().intersects(rectangle);
}
/**
* Returns <code>true</code> if this room contains
* the point at (<code>x</code>, <code>y</code>) with a given <code>margin</code>.
*/
public boolean containsPoint(float x, float y, float margin) {
return containsShapeAtWithMargin(getShape(), x, y, margin);
}
/**
* Returns the index of the point of this room equal to
* the point at (<code>x</code>, <code>y</code>) with a given <code>margin</code>.
* @return the index of the first found point or -1.
*/
public int getPointIndexAt(float x, float y, float margin) {
for (int i = 0; i < this.points.length; i++) {
if (Math.abs(x - this.points [i][0]) <= margin && Math.abs(y - this.points [i][1]) <= margin) {
return i;
}
}
return -1;
}
/**
* Returns <code>true</code> if the center point at which is displayed the name
* of this room is equal to the point at (<code>x</code>, <code>y</code>)
* with a given <code>margin</code>.
*/
public boolean isNameCenterPointAt(float x, float y, float margin) {
return Math.abs(x - getXCenter() - getNameXOffset()) <= margin
&& Math.abs(y - getYCenter() - getNameYOffset()) <= margin;
}
/**
* Returns <code>true</code> if the center point at which is displayed the area
* of this room is equal to the point at (<code>x</code>, <code>y</code>)
* with a given <code>margin</code>.
*/
public boolean isAreaCenterPointAt(float x, float y, float margin) {
return Math.abs(x - getXCenter() - getAreaXOffset()) <= margin
&& Math.abs(y - getYCenter() - getAreaYOffset()) <= margin;
}
/**
* Returns <code>true</code> if <code>shape</code> contains
* the point at (<code>x</code>, <code>y</code>)
* with a given <code>margin</code>.
*/
private boolean containsShapeAtWithMargin(Shape shape, float x, float y, float margin) {
if (margin == 0) {
return shape.contains(x, y);
} else {
return shape.intersects(x - margin, y - margin, 2 * margin, 2 * margin);
}
}
/**
* Returns the shape matching this room.
*/
private Shape getShape() {
if (this.shapeCache == null) {
GeneralPath roomShape = new GeneralPath();
roomShape.moveTo(this.points [0][0], this.points [0][1]);
for (int i = 1; i < this.points.length; i++) {
roomShape.lineTo(this.points [i][0], this.points [i][1]);
}
roomShape.closePath();
// Cache roomShape
this.shapeCache = roomShape;
}
return this.shapeCache;
}
/**
* Moves this room of (<code>dx</code>, <code>dy</code>) units.
*/
public void move(float dx, float dy) {
if (dx != 0 || dy != 0) {
float [][] points = getPoints();
for (int i = 0; i < points.length; i++) {
points [i][0] += dx;
points [i][1] += dy;
}
updatePoints(points);
}
}
/**
* Returns a clone of this room.
*/
@Override
public Room clone() {
try {
Room clone = (Room)super.clone();
clone.propertyChangeSupport = new PropertyChangeSupport(clone);
clone.level = null;
return clone;
} catch (CloneNotSupportedException ex) {
throw new IllegalStateException("Super class isn't cloneable");
}
}
}