/* * Home.java 15 mai 2006 * * Sweet Home 3D, Copyright (c) 2006 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.beans.PropertyChangeEvent; 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.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; /** * The home managed by the application with its furniture and walls. * @author Emmanuel Puybaret */ public class Home implements Serializable, Cloneable { private static final long serialVersionUID = 1L; /** * The current version of this home. Each time the field list is changed * in <code>Home</code> class or in one of the classes that it uses, * this number is increased. */ public static final long CURRENT_VERSION = 4100; private static final boolean KEEP_BACKWARD_COMPATIBLITY = true; private static final Comparator<Level> LEVEL_ELEVATION_COMPARATOR = new Comparator<Level>() { public int compare(Level level1, Level level2) { int elevationComparison = Float.compare(level1.getElevation(), level2.getElevation()); if (elevationComparison != 0) { return elevationComparison; } else { return -Float.compare(level1.getHeight(), level2.getHeight()); } } }; /** * The properties of a home that may change. <code>PropertyChangeListener</code>s added * to a home will be notified under a property name equal to the name value of one these properties. */ public enum Property {NAME, MODIFIED, FURNITURE_SORTED_PROPERTY, FURNITURE_DESCENDING_SORTED, FURNITURE_VISIBLE_PROPERTIES, BACKGROUND_IMAGE, CAMERA, PRINT, BASE_PLAN_LOCKED, STORED_CAMERAS, RECOVERED, SELECTED_LEVEL}; private List<HomePieceOfFurniture> furniture; private transient CollectionChangeSupport<HomePieceOfFurniture> furnitureChangeSupport; private transient List<Selectable> selectedItems; private transient List<SelectionListener> selectionListeners; private List<Level> levels; private Level selectedLevel; private transient CollectionChangeSupport<Level> levelsChangeSupport; private List<Wall> walls; private transient CollectionChangeSupport<Wall> wallsChangeSupport; private List<Room> rooms; private transient CollectionChangeSupport<Room> roomsChangeSupport; private List<DimensionLine> dimensionLines; private transient CollectionChangeSupport<DimensionLine> dimensionLinesChangeSupport; private List<Label> labels; private transient CollectionChangeSupport<Label> labelsChangeSupport; private Camera camera; private String name; private final float wallHeight; private transient boolean modified; private transient boolean recovered; private BackgroundImage backgroundImage; private ObserverCamera observerCamera; private Camera topCamera; private List<Camera> storedCameras; private HomeEnvironment environment; private HomePrint print; private String furnitureSortedPropertyName; private List<String> furnitureVisiblePropertyNames; private boolean furnitureDescendingSorted; private Map<String, Object> visualProperties; private transient PropertyChangeSupport propertyChangeSupport; private long version; private boolean basePlanLocked; private Compass compass; // The 5 following environment fields are still declared for compatibility reasons private int skyColor; private int groundColor; private HomeTexture groundTexture; private int lightColor; private float wallsAlpha; // The two following fields aren't transient for backward compatibility reasons private HomePieceOfFurniture.SortableProperty furnitureSortedProperty; private List<HomePieceOfFurniture.SortableProperty> furnitureVisibleProperties; // The following field is a temporary copy of furniture containing HomeDoorOrWindow instances // created at serialization time for backward compatibility reasons private List<HomePieceOfFurniture> furnitureWithDoorsAndWindows; // The following field is a temporary copy of furniture containing HomeFurnitureGroup instances // created at serialization time for backward compatibility reasons private List<HomePieceOfFurniture> furnitureWithGroups; /** * Creates a home with no furniture, no walls, * and a height equal to 250 cm. */ public Home() { this(250); } /** * Creates a home with no furniture and no walls. */ public Home(float wallHeight) { this(new ArrayList<HomePieceOfFurniture>(), wallHeight); } /** * Creates a home with the given <code>furniture</code>, * no walls and a height equal to 250 cm. */ public Home(List<HomePieceOfFurniture> furniture) { this(furniture, 250); } private Home(List<HomePieceOfFurniture> furniture, float wallHeight) { this.furniture = new ArrayList<HomePieceOfFurniture>(furniture); this.walls = new ArrayList<Wall>(); this.wallHeight = wallHeight; this.furnitureVisibleProperties = Arrays.asList(new HomePieceOfFurniture.SortableProperty [] { HomePieceOfFurniture.SortableProperty.NAME, HomePieceOfFurniture.SortableProperty.WIDTH, HomePieceOfFurniture.SortableProperty.DEPTH, HomePieceOfFurniture.SortableProperty.HEIGHT, HomePieceOfFurniture.SortableProperty.VISIBLE}); // Init transient lists and other fields init(true); addModelListeners(); } /** * Initializes new and transient home fields to their default values * and reads home from <code>in</code> stream with default reading method. */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { init(false); in.defaultReadObject(); addModelListeners(); if (KEEP_BACKWARD_COMPATIBLITY) { // Restore furnitureSortedProperty from furnitureSortedPropertyName if (this.furnitureSortedPropertyName != null) { try { this.furnitureSortedProperty = HomePieceOfFurniture.SortableProperty.valueOf(this.furnitureSortedPropertyName); } catch (IllegalArgumentException ex) { // Ignore malformed enum constant } this.furnitureSortedPropertyName = null; } // Restore furnitureVisibleProperties from furnitureVisiblePropertyNames if (this.furnitureVisiblePropertyNames != null) { this.furnitureVisibleProperties = new ArrayList<HomePieceOfFurniture.SortableProperty>(); for (String furnitureVisiblePropertyName : this.furnitureVisiblePropertyNames) { try { this.furnitureVisibleProperties.add( HomePieceOfFurniture.SortableProperty.valueOf(furnitureVisiblePropertyName)); } catch (IllegalArgumentException ex) { // Ignore malformed enum constants } } this.furnitureVisiblePropertyNames = null; } // Ensure all wall have an height for (Wall wall : this.walls) { if (wall.getHeight() == null) { wall.setHeight(this.wallHeight); } } // Restore referenced HomeDoorOrWindow instances stored in a separate field // for backward compatibility reasons if (this.furnitureWithDoorsAndWindows != null) { this.furniture = this.furnitureWithDoorsAndWindows; this.furnitureWithDoorsAndWindows = null; } // Restore referenced HomeFurnitureGroup instances stored in a separate field // for backward compatibility reasons if (this.furnitureWithGroups != null) { this.furniture = this.furnitureWithGroups; this.furnitureWithGroups = null; } // Restore environment fields from home fields for compatibility reasons this.environment.setGroundColor(this.groundColor); this.environment.setGroundTexture(this.groundTexture); this.environment.setSkyColor(this.skyColor); this.environment.setLightColor(this.lightColor); this.environment.setWallsAlpha(this.wallsAlpha); if (this.version <= 3400) { // Automatically adjust ground color to a darker color int groundColor = this.environment.getGroundColor(); this.environment.setGroundColor( ((((groundColor >> 16) & 0xFF) * 3 / 4) << 16) | ((((groundColor >> 8) & 0xFF) * 3 / 4) << 8) | ((groundColor & 0xFF) * 3 / 4)); } } } private void init(boolean newHome) { // Initialize transient lists this.selectedItems = new ArrayList<Selectable>(); this.furnitureChangeSupport = new CollectionChangeSupport<HomePieceOfFurniture>(this); this.selectionListeners = new ArrayList<SelectionListener>(); this.levelsChangeSupport = new CollectionChangeSupport<Level>(this); this.wallsChangeSupport = new CollectionChangeSupport<Wall>(this); this.roomsChangeSupport = new CollectionChangeSupport<Room>(this); this.dimensionLinesChangeSupport = new CollectionChangeSupport<DimensionLine>(this); this.labelsChangeSupport = new CollectionChangeSupport<Label>(this); this.propertyChangeSupport = new PropertyChangeSupport(this); if (this.furnitureVisibleProperties == null) { // Set the furniture properties that were visible before version 0.19 this.furnitureVisibleProperties = Arrays.asList(new HomePieceOfFurniture.SortableProperty [] { HomePieceOfFurniture.SortableProperty.NAME, HomePieceOfFurniture.SortableProperty.WIDTH, HomePieceOfFurniture.SortableProperty.DEPTH, HomePieceOfFurniture.SortableProperty.HEIGHT, HomePieceOfFurniture.SortableProperty.COLOR, HomePieceOfFurniture.SortableProperty.MOVABLE, HomePieceOfFurniture.SortableProperty.DOOR_OR_WINDOW, HomePieceOfFurniture.SortableProperty.VISIBLE}); } // Create a default top camera that matches default point of view in previous versions this.topCamera = new Camera(500, 1500, 1010, (float)Math.PI, (float)Math.PI / 4, (float)Math.PI * 63 / 180); // Create a default observer camera (use a 63� field of view equivalent to a 35mm lens for a 24x36 film) this.observerCamera = new ObserverCamera(50, 50, 170, 7 * (float)Math.PI / 4, (float)Math.PI / 16, (float)Math.PI * 63 / 180); this.storedCameras = Collections.emptyList(); // Initialize new fields this.environment = new HomeEnvironment(); this.rooms = new ArrayList<Room>(); this.dimensionLines = new ArrayList<DimensionLine>(); this.labels = new ArrayList<Label>(); this.compass = new Compass(-100, 50, 100); this.levels = new ArrayList<Level>(); // Let compass be visible only on new homes this.compass.setVisible(newHome); this.visualProperties = new HashMap<String, Object>(); this.version = CURRENT_VERSION; } /** * Adds listeners to model. */ private void addModelListeners() { // Add listeners to levels to maintain its elevation order final PropertyChangeListener levelElevationChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent ev) { if (Level.Property.ELEVATION.name().equals(ev.getPropertyName()) || Level.Property.HEIGHT.name().equals(ev.getPropertyName())) { levels = new ArrayList<Level>(levels); Collections.sort(levels, LEVEL_ELEVATION_COMPARATOR); } } }; for (Level level : this.levels) { level.addPropertyChangeListener(levelElevationChangeListener); } addLevelsListener(new CollectionListener<Level>() { public void collectionChanged(CollectionEvent<Level> ev) { switch (ev.getType()) { case ADD : ev.getItem().addPropertyChangeListener(levelElevationChangeListener); break; case DELETE : ev.getItem().removePropertyChangeListener(levelElevationChangeListener); break; } } }); } /** * Sets the version of this home and writes it to <code>out</code> stream * with default writing method. */ private void writeObject(java.io.ObjectOutputStream out) throws IOException { this.version = CURRENT_VERSION; if (KEEP_BACKWARD_COMPATIBLITY) { HomePieceOfFurniture.SortableProperty homeFurnitureSortedProperty = this.furnitureSortedProperty; List<HomePieceOfFurniture.SortableProperty> homeFurnitureVisibleProperties = this.furnitureVisibleProperties; List<HomePieceOfFurniture> homeFurniture = this.furniture; try { if (this.furnitureSortedProperty != null) { this.furnitureSortedPropertyName = this.furnitureSortedProperty.name(); // Store in furnitureSortedProperty only backward compatible property if (!isFurnitureSortedPropertyBackwardCompatible(this.furnitureSortedProperty)) { this.furnitureSortedProperty = null; } } this.furnitureVisiblePropertyNames = new ArrayList<String>(); // Store in furnitureVisibleProperties only backward compatible properties this.furnitureVisibleProperties = new ArrayList<HomePieceOfFurniture.SortableProperty>(); for (HomePieceOfFurniture.SortableProperty visibleProperty : homeFurnitureVisibleProperties) { this.furnitureVisiblePropertyNames.add(visibleProperty.name()); if (isFurnitureSortedPropertyBackwardCompatible(visibleProperty)) { this.furnitureVisibleProperties.add(visibleProperty); } } // Store referenced HomeFurnitureGroup instances in a separate field // for backward compatibility reasons (version < 2.3) this.furnitureWithGroups = this.furniture; // Serialize a furnitureWithDoorsAndWindows field that contains only // HomePieceOfFurniture, HomeDoorOrWindow and HomeLight instances // for backward compatibility reasons (version < 1.7) this.furnitureWithDoorsAndWindows = new ArrayList<HomePieceOfFurniture>(this.furniture.size()); // Serialize a furniture field that contains only HomePieceOfFurniture instances this.furniture = new ArrayList<HomePieceOfFurniture>(this.furniture.size()); for (HomePieceOfFurniture piece : this.furnitureWithGroups) { if (piece.getClass() == HomePieceOfFurniture.class) { this.furnitureWithDoorsAndWindows.add(piece); this.furniture.add(piece); } else { if (piece.getClass() == HomeFurnitureGroup.class) { // Add the ungrouped pieces to furniture and furnitureWithDoorsAndWindows list for (HomePieceOfFurniture groupPiece : getGroupFurniture((HomeFurnitureGroup)piece)) { this.furnitureWithDoorsAndWindows.add(groupPiece); if (groupPiece.getClass() == HomePieceOfFurniture.class) { this.furniture.add(groupPiece); } else { // Create backward compatible instances this.furniture.add(new HomePieceOfFurniture(groupPiece)); } } } else { this.furnitureWithDoorsAndWindows.add(piece); // Create backward compatible instances this.furniture.add(new HomePieceOfFurniture(piece)); } } } // Store environment fields in home fields for compatibility reasons this.groundColor = this.environment.getGroundColor(); this.groundTexture = this.environment.getGroundTexture(); this.skyColor = this.environment.getSkyColor(); this.lightColor = this.environment.getLightColor(); this.wallsAlpha = this.environment.getWallsAlpha(); out.defaultWriteObject(); } finally { // Restore home values this.furniture = homeFurniture; this.furnitureWithDoorsAndWindows = null; this.furnitureWithGroups = null; this.furnitureSortedProperty = homeFurnitureSortedProperty; this.furnitureVisibleProperties = homeFurnitureVisibleProperties; // Set furnitureSortedPropertyName and furnitureVisiblePropertyNames to null // (they are used only for serialization) this.furnitureSortedPropertyName = null; this.furnitureVisiblePropertyNames = null; } } else { out.defaultWriteObject(); } } /** * Returns <code>true</code> if the given <code>property</code> is compatible * with the first set of sortable properties that existed in <code>HomePieceOfFurniture</code> class. */ private boolean isFurnitureSortedPropertyBackwardCompatible(HomePieceOfFurniture.SortableProperty property) { switch (property) { case NAME : case WIDTH : case DEPTH : case HEIGHT : case MOVABLE : case DOOR_OR_WINDOW : case COLOR : case VISIBLE : case X : case Y : case ELEVATION : case ANGLE : return true; default : return false; } } /** * Returns all the pieces of the given <code>furnitureGroup</code>. */ private List<HomePieceOfFurniture> getGroupFurniture(HomeFurnitureGroup furnitureGroup) { List<HomePieceOfFurniture> groupFurniture = new ArrayList<HomePieceOfFurniture>(); for (HomePieceOfFurniture piece : furnitureGroup.getFurniture()) { if (piece instanceof HomeFurnitureGroup) { groupFurniture.addAll(getGroupFurniture((HomeFurnitureGroup)piece)); } else { groupFurniture.add(piece); } } return groupFurniture; } /** * Adds the level <code>listener</code> in parameter to this home. * @since 3.4 */ public void addLevelsListener(CollectionListener<Level> listener) { this.levelsChangeSupport.addCollectionListener(listener); } /** * Removes the level <code>listener</code> in parameter from this home. * @since 3.4 */ public void removeLevelsListener(CollectionListener<Level> listener) { this.levelsChangeSupport.removeCollectionListener(listener); } /** * Returns an unmodifiable collection of the levels of this home. * @since 3.4 */ public List<Level> getLevels() { return Collections.unmodifiableList(this.levels); } /** * Adds the given <code>level</code> to the list of levels of this home. * Once the <code>level</code> is added, level listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#ADD ADD}. * @since 3.4 */ public void addLevel(Level level) { // Make a copy of the list to avoid conflicts in the list returned by getLevels this.levels = new ArrayList<Level>(this.levels); // Search at which index should be inserted the new level int index = Collections.binarySearch(this.levels, level, LEVEL_ELEVATION_COMPARATOR); int levelIndex; if (index >= 0) { levelIndex = index; } else { levelIndex = -(index + 1); } this.levels.add(levelIndex, level); this.levelsChangeSupport.fireCollectionChanged(level, levelIndex, CollectionEvent.Type.ADD); } /** * Removes the given <code>level</code> from the set of levels of this home * and all the furniture, walls, rooms, dimension lines and labels that belong to this level. * Once the <code>level</code> is removed, level listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#DELETE DELETE}. * @since 3.4 */ public void deleteLevel(Level level) { int index = this.levels.indexOf(level); if (index != -1) { for (HomePieceOfFurniture piece : this.furniture) { if (piece.getLevel() == level) { deletePieceOfFurniture(piece); } } for (Room room : this.rooms) { if (room.getLevel() == level) { deleteRoom(room); } } for (Wall wall : this.walls) { if (wall.getLevel() == level) { deleteWall(wall); } } for (DimensionLine dimensionLine : this.dimensionLines) { if (dimensionLine.getLevel() == level) { deleteDimensionLine(dimensionLine); } } for (Label label : this.labels) { if (label.getLevel() == level) { deleteLabel(label); } } if (this.selectedLevel == level) { if (this.levels.size() == 1) { setSelectedLevel(null); } else { setSelectedLevel(this.levels.get(index >= 1 ? index - 1 : index + 1)); } } // Make a copy of the list to avoid conflicts in the list returned by getLevels this.levels = new ArrayList<Level>(this.levels); this.levels.remove(index); this.levelsChangeSupport.fireCollectionChanged(level, index, CollectionEvent.Type.DELETE); } } /** * Returns the selected level in home or <code>null</code> if home has no level. * @since 3.4 */ public Level getSelectedLevel() { return this.selectedLevel; } /** * Sets the selected level in home and notifies listeners of the change. * @since 3.4 */ public void setSelectedLevel(Level selectedLevel) { if (selectedLevel != this.selectedLevel) { Level oldSelectedLevel = this.selectedLevel; this.selectedLevel = selectedLevel; this.propertyChangeSupport.firePropertyChange(Property.SELECTED_LEVEL.name(), oldSelectedLevel, selectedLevel); } } /** * Adds the furniture <code>listener</code> in parameter to this home. */ public void addFurnitureListener(CollectionListener<HomePieceOfFurniture> listener) { this.furnitureChangeSupport.addCollectionListener(listener); } /** * Removes the furniture <code>listener</code> in parameter from this home. */ public void removeFurnitureListener(CollectionListener<HomePieceOfFurniture> listener) { this.furnitureChangeSupport.removeCollectionListener(listener); } /** * Returns an unmodifiable list of the furniture managed by this home. * This furniture in this list is always sorted in the index order they were added to home. */ public List<HomePieceOfFurniture> getFurniture() { return Collections.unmodifiableList(this.furniture); } /** * Adds the <code>piece</code> in parameter to this home. * Once the <code>piece</code> is added, furniture listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification. */ public void addPieceOfFurniture(HomePieceOfFurniture piece) { addPieceOfFurniture(piece, this.furniture.size()); } /** * Adds the <code>piece</code> in parameter at a given <code>index</code>. * Once the <code>piece</code> is added, furniture listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification. */ public void addPieceOfFurniture(HomePieceOfFurniture piece, int index) { // Make a copy of the list to avoid conflicts in the list returned by getFurniture this.furniture = new ArrayList<HomePieceOfFurniture>(this.furniture); piece.setLevel(this.selectedLevel); this.furniture.add(index, piece); this.furnitureChangeSupport.fireCollectionChanged(piece, index, CollectionEvent.Type.ADD); } /** * Deletes the <code>piece</code> in parameter from this home. * Once the <code>piece</code> is deleted, furniture listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification. */ public void deletePieceOfFurniture(HomePieceOfFurniture piece) { // Ensure selectedItems don't keep a reference to piece deselectItem(piece); int index = this.furniture.indexOf(piece); if (index != -1) { piece.setLevel(null); // Make a copy of the list to avoid conflicts in the list returned by getFurniture this.furniture = new ArrayList<HomePieceOfFurniture>(this.furniture); this.furniture.remove(index); this.furnitureChangeSupport.fireCollectionChanged(piece, index, CollectionEvent.Type.DELETE); } } /** * Adds the selection <code>listener</code> in parameter to this home. */ public void addSelectionListener(SelectionListener listener) { this.selectionListeners.add(listener); } /** * Removes the selection <code>listener</code> in parameter from this home. */ public void removeSelectionListener(SelectionListener listener) { this.selectionListeners.remove(listener); } /** * Returns an unmodifiable list of the selected items in home. */ public List<Selectable> getSelectedItems() { return Collections.unmodifiableList(this.selectedItems); } /** * Sets the selected items in home and notifies listeners selection change. */ public void setSelectedItems(List<? extends Selectable> selectedItems) { // Make a copy of the list to avoid conflicts in the list returned by getSelectedItems this.selectedItems = new ArrayList<Selectable>(selectedItems); if (!this.selectionListeners.isEmpty()) { SelectionEvent selectionEvent = new SelectionEvent(this, getSelectedItems()); // Work on a copy of selectionListeners to ensure a listener // can modify safely listeners list SelectionListener [] listeners = this.selectionListeners. toArray(new SelectionListener [this.selectionListeners.size()]); for (SelectionListener listener : listeners) { listener.selectionChanged(selectionEvent); } } } /** * Deselects <code>item</code> if it's selected and notifies listeners selection change. * @since 2.2 */ public void deselectItem(Selectable item) { int pieceSelectionIndex = this.selectedItems.indexOf(item); if (pieceSelectionIndex != -1) { List<Selectable> selectedItems = new ArrayList<Selectable>(getSelectedItems()); selectedItems.remove(pieceSelectionIndex); setSelectedItems(selectedItems); } } /** * Adds the room <code>listener</code> in parameter to this home. */ public void addRoomsListener(CollectionListener<Room> listener) { this.roomsChangeSupport.addCollectionListener(listener); } /** * Removes the room <code>listener</code> in parameter from this home. */ public void removeRoomsListener(CollectionListener<Room> listener) { this.roomsChangeSupport.removeCollectionListener(listener); } /** * Returns an unmodifiable collection of the rooms of this home. */ public List<Room> getRooms() { return Collections.unmodifiableList(this.rooms); } /** * Adds the given <code>room</code> to the list of rooms of this home. * Once the <code>room</code> is added, room listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#ADD ADD}. */ public void addRoom(Room room) { addRoom(room, this.rooms.size()); } /** * Adds the <code>room</code> in parameter at a given <code>index</code>. * Once the <code>room</code> is added, room listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#ADD ADD}. */ public void addRoom(Room room, int index) { // Make a copy of the list to avoid conflicts in the list returned by getRooms this.rooms = new ArrayList<Room>(this.rooms); this.rooms.add(index, room); room.setLevel(this.selectedLevel); this.roomsChangeSupport.fireCollectionChanged(room, index, CollectionEvent.Type.ADD); } /** * Removes the given <code>room</code> from the set of rooms of this home. * Once the <code>room</code> is removed, room listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#DELETE DELETE}. */ public void deleteRoom(Room room) { // Ensure selectedItems don't keep a reference to room deselectItem(room); int index = this.rooms.indexOf(room); if (index != -1) { room.setLevel(null); // Make a copy of the list to avoid conflicts in the list returned by getRooms this.rooms = new ArrayList<Room>(this.rooms); this.rooms.remove(index); this.roomsChangeSupport.fireCollectionChanged(room, index, CollectionEvent.Type.DELETE); } } /** * Adds the wall <code>listener</code> in parameter to this home. */ public void addWallsListener(CollectionListener<Wall> listener) { this.wallsChangeSupport.addCollectionListener(listener); } /** * Removes the wall <code>listener</code> in parameter from this home. */ public void removeWallsListener(CollectionListener<Wall> listener) { this.wallsChangeSupport.removeCollectionListener(listener); } /** * Returns an unmodifiable collection of the walls of this home. */ public Collection<Wall> getWalls() { return Collections.unmodifiableCollection(this.walls); } /** * Adds the given <code>wall</code> to the set of walls of this home. * Once the <code>wall</code> is added, wall listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#ADD ADD}. */ public void addWall(Wall wall) { // Make a copy of the list to avoid conflicts in the list returned by getWalls this.walls = new ArrayList<Wall>(this.walls); this.walls.add(wall); wall.setLevel(this.selectedLevel); this.wallsChangeSupport.fireCollectionChanged(wall, CollectionEvent.Type.ADD); } /** * Removes the given <code>wall</code> from the set of walls of this home. * Once the <code>wall</code> is removed, wall listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#DELETE DELETE}. * If any wall is attached to <code>wall</code> they will be detached from it. */ public void deleteWall(Wall wall) { // Ensure selectedItems don't keep a reference to wall deselectItem(wall); // Detach any other wall attached to wall for (Wall otherWall : getWalls()) { if (wall.equals(otherWall.getWallAtStart())) { otherWall.setWallAtStart(null); } else if (wall.equals(otherWall.getWallAtEnd())) { otherWall.setWallAtEnd(null); } } int index = this.walls.indexOf(wall); if (index != -1) { wall.setLevel(null); // Make a copy of the list to avoid conflicts in the list returned by getWalls this.walls = new ArrayList<Wall>(this.walls); this.walls.remove(index); this.wallsChangeSupport.fireCollectionChanged(wall, CollectionEvent.Type.DELETE); } } /** * Adds the dimension line <code>listener</code> in parameter to this home. */ public void addDimensionLinesListener(CollectionListener<DimensionLine> listener) { this.dimensionLinesChangeSupport.addCollectionListener(listener); } /** * Removes the dimension line <code>listener</code> in parameter from this home. */ public void removeDimensionLinesListener(CollectionListener<DimensionLine> listener) { this.dimensionLinesChangeSupport.removeCollectionListener(listener); } /** * Returns an unmodifiable collection of the dimension lines of this home. */ public Collection<DimensionLine> getDimensionLines() { return Collections.unmodifiableCollection(this.dimensionLines); } /** * Adds the given dimension line to the set of dimension lines of this home. * Once <code>dimensionLine</code> is added, dimension line listeners added * to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#ADD ADD}. */ public void addDimensionLine(DimensionLine dimensionLine) { // Make a copy of the list to avoid conflicts in the list returned by getDimensionLines this.dimensionLines = new ArrayList<DimensionLine>(this.dimensionLines); this.dimensionLines.add(dimensionLine); dimensionLine.setLevel(this.selectedLevel); this.dimensionLinesChangeSupport.fireCollectionChanged(dimensionLine, CollectionEvent.Type.ADD); } /** * Removes the given dimension line from the set of dimension lines of this home. * Once <code>dimensionLine</code> is removed, dimension line listeners added * to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#DELETE DELETE}. */ public void deleteDimensionLine(DimensionLine dimensionLine) { // Ensure selectedItems don't keep a reference to dimension line deselectItem(dimensionLine); int index = this.dimensionLines.indexOf(dimensionLine); if (index != -1) { dimensionLine.setLevel(null); // Make a copy of the list to avoid conflicts in the list returned by getDimensionLines this.dimensionLines = new ArrayList<DimensionLine>(this.dimensionLines); this.dimensionLines.remove(index); this.dimensionLinesChangeSupport.fireCollectionChanged(dimensionLine, CollectionEvent.Type.DELETE); } } /** * Adds the label <code>listener</code> in parameter to this home. */ public void addLabelsListener(CollectionListener<Label> listener) { this.labelsChangeSupport.addCollectionListener(listener); } /** * Removes the label <code>listener</code> in parameter from this home. */ public void removeLabelsListener(CollectionListener<Label> listener) { this.labelsChangeSupport.removeCollectionListener(listener); } /** * Returns an unmodifiable collection of the labels of this home. */ public Collection<Label> getLabels() { return Collections.unmodifiableCollection(this.labels); } /** * Adds the given label to the set of labels of this home. * Once <code>label</code> is added, label listeners added * to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#ADD ADD}. */ public void addLabel(Label label) { // Make a copy of the list to avoid conflicts in the list returned by getLabels this.labels = new ArrayList<Label>(this.labels); this.labels.add(label); label.setLevel(this.selectedLevel); this.labelsChangeSupport.fireCollectionChanged(label, CollectionEvent.Type.ADD); } /** * Removes the given label from the set of labels of this home. * Once <code>label</code> is removed, label listeners added to this home will receive a * {@link CollectionListener#collectionChanged(CollectionEvent) collectionChanged} * notification, with an {@link CollectionEvent#getType() event type} * equal to {@link CollectionEvent.Type#DELETE DELETE}. */ public void deleteLabel(Label label) { // Ensure selectedItems don't keep a reference to label deselectItem(label); int index = this.labels.indexOf(label); if (index != -1) { label.setLevel(null); // Make a copy of the list to avoid conflicts in the list returned by getLabels this.labels = new ArrayList<Label>(this.labels); this.labels.remove(index); this.labelsChangeSupport.fireCollectionChanged(label, CollectionEvent.Type.DELETE); } } /** * Returns <code>true</code> if this home doesn't contain any item i.e. * no piece of furniture, no wall, no room, no dimension line and no label. * @since 2.2 */ public boolean isEmpty() { return this.furniture.isEmpty() && this.walls.isEmpty() && this.rooms.isEmpty() && this.dimensionLines.isEmpty() && this.labels.isEmpty(); } /** * Adds the property change <code>listener</code> in parameter to this home. */ public void addPropertyChangeListener(Property property, PropertyChangeListener listener) { this.propertyChangeSupport.addPropertyChangeListener(property.name(), listener); } /** * Removes the property change <code>listener</code> in parameter from this home. */ public void removePropertyChangeListener(Property property, PropertyChangeListener listener) { this.propertyChangeSupport.removePropertyChangeListener(property.name(), listener); } /** * Returns the wall height of this home. */ public float getWallHeight() { return this.wallHeight; } /** * Returns the name of this home. */ public String getName() { return this.name; } /** * Sets the name of this home and fires a <code>PropertyChangeEvent</code>. */ 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 whether the state of this home is modified or not. */ public boolean isModified() { return this.modified; } /** * Sets the modified state of this home and fires a <code>PropertyChangeEvent</code>. */ public void setModified(boolean modified) { if (modified != this.modified) { this.modified = modified; this.propertyChangeSupport.firePropertyChange( Property.MODIFIED.name(), !modified, modified); } } /** * Returns whether this home was recovered or not. * @since 3.0 */ public boolean isRecovered() { return this.recovered; } /** * Sets whether this home was recovered or not and fires a <code>PropertyChangeEvent</code>. * @since 3.0 */ public void setRecovered(boolean recovered) { if (recovered != this.recovered) { this.recovered = recovered; this.propertyChangeSupport.firePropertyChange( Property.RECOVERED.name(), !recovered, recovered); } } /** * Returns the furniture property on which home is sorted or <code>null</code> if * home furniture isn't sorted. */ public HomePieceOfFurniture.SortableProperty getFurnitureSortedProperty() { return this.furnitureSortedProperty; } /** * Sets the furniture property on which this home should be sorted * and fires a <code>PropertyChangeEvent</code>. */ public void setFurnitureSortedProperty(HomePieceOfFurniture.SortableProperty furnitureSortedProperty) { if (furnitureSortedProperty != this.furnitureSortedProperty && (furnitureSortedProperty == null || !furnitureSortedProperty.equals(this.furnitureSortedProperty))) { HomePieceOfFurniture.SortableProperty oldFurnitureSortedProperty = this.furnitureSortedProperty; this.furnitureSortedProperty = furnitureSortedProperty; this.propertyChangeSupport.firePropertyChange( Property.FURNITURE_SORTED_PROPERTY.name(), oldFurnitureSortedProperty, furnitureSortedProperty); } } /** * Returns whether furniture is sorted in ascending or descending order. */ public boolean isFurnitureDescendingSorted() { return this.furnitureDescendingSorted; } /** * Sets the furniture sort order on which home should be sorted * and fires a <code>PropertyChangeEvent</code>. */ public void setFurnitureDescendingSorted(boolean furnitureDescendingSorted) { if (furnitureDescendingSorted != this.furnitureDescendingSorted) { this.furnitureDescendingSorted = furnitureDescendingSorted; this.propertyChangeSupport.firePropertyChange( Property.FURNITURE_DESCENDING_SORTED.name(), !furnitureDescendingSorted, furnitureDescendingSorted); } } /** * Returns an unmodifiable list of the furniture properties that are visible. */ public List<HomePieceOfFurniture.SortableProperty> getFurnitureVisibleProperties() { if (this.furnitureVisibleProperties == null) { return Collections.emptyList(); } else { return Collections.unmodifiableList(this.furnitureVisibleProperties); } } /** * Sets the furniture properties that are visible and the order in which they are visible, * then fires a <code>PropertyChangeEvent</code>. */ public void setFurnitureVisibleProperties(List<HomePieceOfFurniture.SortableProperty> furnitureVisibleProperties) { if (furnitureVisibleProperties != this.furnitureVisibleProperties && (furnitureVisibleProperties == null || !furnitureVisibleProperties.equals(this.furnitureVisibleProperties))) { List<HomePieceOfFurniture.SortableProperty> oldFurnitureVisibleProperties = this.furnitureVisibleProperties; this.furnitureVisibleProperties = new ArrayList<HomePieceOfFurniture.SortableProperty>(furnitureVisibleProperties); this.propertyChangeSupport.firePropertyChange( Property.FURNITURE_VISIBLE_PROPERTIES.name(), Collections.unmodifiableList(oldFurnitureVisibleProperties), Collections.unmodifiableList(furnitureVisibleProperties)); } } /** * Returns the plan background image of this home. */ public BackgroundImage getBackgroundImage() { return this.backgroundImage; } /** * Sets the plan background image of this home and fires a <code>PropertyChangeEvent</code>. */ public void setBackgroundImage(BackgroundImage backgroundImage) { if (backgroundImage != this.backgroundImage) { BackgroundImage oldBackgroundImage = this.backgroundImage; this.backgroundImage = backgroundImage; this.propertyChangeSupport.firePropertyChange( Property.BACKGROUND_IMAGE.name(), oldBackgroundImage, backgroundImage); } } /** * Returns the camera used to display this home from a top point of view. */ public Camera getTopCamera() { return this.topCamera; } /** * Returns the camera used to display this home from an observer point of view. */ public ObserverCamera getObserverCamera() { return this.observerCamera; } /** * Sets the camera used to display this home and fires a <code>PropertyChangeEvent</code>. */ public void setCamera(Camera camera) { if (camera != this.camera) { Camera oldCamera = this.camera; this.camera = camera; this.propertyChangeSupport.firePropertyChange( Property.CAMERA.name(), oldCamera, camera); } } /** * Returns the camera used to display this home. */ public Camera getCamera() { if (this.camera == null) { // Use by default top camera this.camera = getTopCamera(); } return this.camera; } /** * Sets the cameras stored by this home and fires a <code>PropertyChangeEvent</code>. * The list given as parameter is cloned but not the camera instances it contains. * @since 3.0 */ public void setStoredCameras(List<Camera> storedCameras) { if (!this.storedCameras.equals(storedCameras)) { List<Camera> oldStoredCameras = this.storedCameras; if (storedCameras == null) { this.storedCameras = Collections.emptyList(); } else { this.storedCameras = new ArrayList<Camera>(storedCameras); } this.propertyChangeSupport.firePropertyChange( Property.STORED_CAMERAS.name(), Collections.unmodifiableList(oldStoredCameras), Collections.unmodifiableList(storedCameras)); } } /** * Returns an unmodifiable list of the cameras stored by this home. * @since 3.0 */ public List<Camera> getStoredCameras() { return Collections.unmodifiableList(this.storedCameras); } /** * Returns the environment attributes of this home. */ public HomeEnvironment getEnvironment() { return this.environment; } /** * Returns the compass associated to this home. * @since 3.0 */ public Compass getCompass() { return this.compass; } /** * Returns the print attributes of this home. */ public HomePrint getPrint() { return this.print; } /** * Sets the print attributes of this home and fires a <code>PropertyChangeEvent</code>. */ public void setPrint(HomePrint print) { if (print != this.print) { HomePrint oldPrint = this.print; this.print = print; this.propertyChangeSupport.firePropertyChange(Property.PRINT.name(), oldPrint, print); } this.print = print; } /** * Returns the value of the visual property <code>propertyName</code> associated with this home. */ public Object getVisualProperty(String propertyName) { return this.visualProperties.get(propertyName); } /** * Sets a visual property associated with this home. */ public void setVisualProperty(String propertyName, Object propertyValue) { this.visualProperties.put(propertyName, propertyValue); } /** * Returns <code>true</code> if the home objects belonging to the base plan * (generally walls, rooms, dimension lines and texts) are locked. * @since 1.8 */ public boolean isBasePlanLocked() { return this.basePlanLocked; } /** * Sets whether home objects belonging to the base plan (generally walls, rooms, * dimension lines and texts) are locked and fires a <code>PropertyChangeEvent</code>. * @since 1.8 */ public void setBasePlanLocked(boolean basePlanLocked) { if (basePlanLocked != this.basePlanLocked) { this.basePlanLocked = basePlanLocked; this.propertyChangeSupport.firePropertyChange( Property.BASE_PLAN_LOCKED.name(), !basePlanLocked, basePlanLocked); } } /** * Returns the version of this home, the last time it was serialized or * or {@link #CURRENT_VERSION} if it is not serialized yet or * was serialized with Sweet Home 3D 0.x. * Version is useful to know with which Sweet Home 3D version this home was saved * and warn user that he may lose information if he saves with * current application a home created by a more recent version. */ public long getVersion() { return this.version; } /** * Returns a clone of this home and the objects it contains. * Listeners bound to this home aren't added to the returned home. * @since 2.3 */ @Override public Home clone() { try { Home clone = (Home)super.clone(); // Deep clone selectable items clone.selectedItems = new ArrayList<Selectable>(this.selectedItems.size()); clone.furniture = cloneSelectableItems( this.furniture, this.selectedItems, clone.selectedItems); clone.rooms = cloneSelectableItems(this.rooms, this.selectedItems, clone.selectedItems); clone.dimensionLines = cloneSelectableItems( this.dimensionLines, this.selectedItems, clone.selectedItems); clone.labels = cloneSelectableItems(this.labels, this.selectedItems, clone.selectedItems); // Deep clone walls clone.walls = Wall.clone(this.walls); for (int i = 0; i < this.walls.size(); i++) { Wall wall = this.walls.get(i); if (this.selectedItems.contains(wall)) { clone.selectedItems.add(clone.walls.get(i)); } } // Clone levels and set the level of cloned objects clone.levels = new ArrayList<Level>(); if (this.levels.size() > 0) { for (Level level : this.levels) { clone.levels.add(level.clone()); } for (int i = 0; i < this.furniture.size(); i++) { Level pieceLevel = this.furniture.get(i).getLevel(); if (pieceLevel != null) { // As soon as there's more than one level, every object is supposed to have its level set // but as level can still be null for a undetermined reason, prefer to keep level // to null in the cloned object and having errors further than throwing exception here clone.furniture.get(i).setLevel(clone.levels.get(this.levels.indexOf(pieceLevel))); } } for (int i = 0; i < this.rooms.size(); i++) { Level roomLevel = this.rooms.get(i).getLevel(); if (roomLevel != null) { clone.rooms.get(i).setLevel(clone.levels.get(this.levels.indexOf(roomLevel))); } } for (int i = 0; i < this.dimensionLines.size(); i++) { Level dimensionLineLevel = this.dimensionLines.get(i).getLevel(); if (dimensionLineLevel != null) { clone.dimensionLines.get(i).setLevel(clone.levels.get(this.levels.indexOf(dimensionLineLevel))); } } for (int i = 0; i < this.labels.size(); i++) { Level labelLevel = this.labels.get(i).getLevel(); if (labelLevel != null) { clone.labels.get(i).setLevel(clone.levels.get(this.levels.indexOf(labelLevel))); } } for (int i = 0; i < this.walls.size(); i++) { Level wallLevel = this.walls.get(i).getLevel(); if (wallLevel != null) { clone.walls.get(i).setLevel(clone.levels.get(this.levels.indexOf(wallLevel))); } } if (this.selectedLevel != null) { clone.selectedLevel = clone.levels.get(this.levels.indexOf(this.selectedLevel)); } } // Clone cameras clone.observerCamera = this.observerCamera.clone(); clone.topCamera = this.topCamera.clone(); if (this.camera == this.observerCamera) { clone.camera = clone.observerCamera; if (this.selectedItems.contains(this.observerCamera)) { clone.selectedItems.add(clone.observerCamera); } } else { clone.camera = clone.topCamera; } clone.storedCameras = new ArrayList<Camera>(this.storedCameras.size()); for (Camera camera : this.storedCameras) { clone.storedCameras.add(camera.clone()); } // Clone other mutable objects clone.environment = this.environment.clone(); clone.compass = this.compass.clone(); clone.furnitureVisibleProperties = new ArrayList<HomePieceOfFurniture.SortableProperty>( this.furnitureVisibleProperties); clone.visualProperties = new HashMap<String, Object>(this.visualProperties); // Create new listeners support clone.furnitureChangeSupport = new CollectionChangeSupport<HomePieceOfFurniture>(clone); clone.selectionListeners = new ArrayList<SelectionListener>(); clone.wallsChangeSupport = new CollectionChangeSupport<Wall>(clone); clone.roomsChangeSupport = new CollectionChangeSupport<Room>(clone); clone.dimensionLinesChangeSupport = new CollectionChangeSupport<DimensionLine>(clone); clone.labelsChangeSupport = new CollectionChangeSupport<Label>(clone); clone.propertyChangeSupport = new PropertyChangeSupport(clone); return clone; } catch (CloneNotSupportedException ex) { throw new IllegalStateException("Super class isn't cloneable"); } } /** * Returns the list of cloned items in <code>source</code>. * If a cloned item is selected its clone will be selected too (ie added to * <code>destinationSelectedItems</code>). */ @SuppressWarnings("unchecked") private <T extends Selectable> List<T> cloneSelectableItems(List<T> source, List<Selectable> sourceSelectedItems, List<Selectable> destinationSelectedItems) { List<T> destination = new ArrayList<T>(source.size()); for (T item : source) { T clone = (T)item.clone(); destination.add(clone); if (sourceSelectedItems.contains(item)) { destinationSelectedItems.add(clone); } } return destination; } /** * Returns a deep copy of home selectable <code>items</code>. */ public static List<Selectable> duplicate(List<? extends Selectable> items) { List<Selectable> list = new ArrayList<Selectable>(); for (Selectable item : items) { if (!(item instanceof Wall) // Walls are copied further && !(item instanceof Camera) // Cameras and compass can't be duplicated && !(item instanceof Compass)) { list.add(item.clone()); } } // Add to list a clone of walls list with their walls at start and end point set list.addAll(Wall.clone(getWallsSubList(items))); return list; } /** * Returns a sub list of <code>items</code> that contains only home furniture. */ public static List<HomePieceOfFurniture> getFurnitureSubList(List<? extends Selectable> items) { return getSubList(items, HomePieceOfFurniture.class); } /** * Returns a sub list of <code>items</code> that contains only walls. */ public static List<Wall> getWallsSubList(List<? extends Selectable> items) { return getSubList(items, Wall.class); } /** * Returns a sub list of <code>items</code> that contains only rooms. */ public static List<Room> getRoomsSubList(List<? extends Selectable> items) { return getSubList(items, Room.class); } /** * Returns a sub list of <code>items</code> that contains only dimension lines. */ public static List<DimensionLine> getDimensionLinesSubList(List<? extends Selectable> items) { return getSubList(items, DimensionLine.class); } /** * Returns a sub list of <code>items</code> that contains only labels. */ public static List<Label> getLabelsSubList(List<? extends Selectable> items) { return getSubList(items, Label.class); } /** * Returns a sub list of <code>items</code> that contains only instances of <code>subListClass</code>. * @since 2.2 */ @SuppressWarnings("unchecked") public static <T> List<T> getSubList(List<? extends Selectable> items, Class<T> subListClass) { List<T> subList = new ArrayList<T>(); for (Selectable item : items) { if (subListClass.isInstance(item)) { subList.add((T)item); } } return subList; } }