/* * FurnitureController.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.viewcontroller; import java.awt.geom.Line2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import javax.swing.undo.AbstractUndoableEdit; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoableEdit; import javax.swing.undo.UndoableEditSupport; import com.eteks.sweethome3d.model.CollectionEvent; import com.eteks.sweethome3d.model.CollectionListener; import com.eteks.sweethome3d.model.DoorOrWindow; import com.eteks.sweethome3d.model.Home; import com.eteks.sweethome3d.model.HomeDoorOrWindow; import com.eteks.sweethome3d.model.HomeFurnitureGroup; import com.eteks.sweethome3d.model.HomeLight; import com.eteks.sweethome3d.model.HomePieceOfFurniture; import com.eteks.sweethome3d.model.HomePieceOfFurniture.SortableProperty; import com.eteks.sweethome3d.model.Level; import com.eteks.sweethome3d.model.Light; import com.eteks.sweethome3d.model.PieceOfFurniture; import com.eteks.sweethome3d.model.Selectable; import com.eteks.sweethome3d.model.SelectionEvent; import com.eteks.sweethome3d.model.SelectionListener; import com.eteks.sweethome3d.model.UserPreferences; /** * A MVC controller for the home furniture table. * @author Emmanuel Puybaret */ public class FurnitureController implements Controller { private final Home home; private final UserPreferences preferences; private final ViewFactory viewFactory; private final ContentManager contentManager; private final UndoableEditSupport undoSupport; private View furnitureView; private HomePieceOfFurniture leadSelectedPieceOfFurniture; /** * Creates the controller of home furniture view. * @param home the home edited by this controller and its view * @param preferences the preferences of the application * @param viewFactory a factory able to create the furniture view managed by this controller */ public FurnitureController(Home home, UserPreferences preferences, ViewFactory viewFactory) { this(home, preferences, viewFactory, null, null); } /** * Creates the controller of home furniture view with undo support. */ public FurnitureController(final Home home, UserPreferences preferences, ViewFactory viewFactory, ContentManager contentManager, UndoableEditSupport undoSupport) { this.home = home; this.preferences = preferences; this.viewFactory = viewFactory; this.undoSupport = undoSupport; this.contentManager = contentManager; addModelListeners(); } /** * Returns the view associated with this controller. */ public View getView() { // Create view lazily only once it's needed if (this.furnitureView == null) { this.furnitureView = this.viewFactory.createFurnitureView(this.home, this.preferences, this); } return this.furnitureView; } private void addModelListeners() { // Add a selection listener that gets the lead selected piece in home this.home.addSelectionListener(new SelectionListener() { public void selectionChanged(SelectionEvent ev) { List<HomePieceOfFurniture> selectedFurniture = Home.getFurnitureSubList(home.getSelectedItems()); if (selectedFurniture.isEmpty()) { leadSelectedPieceOfFurniture = null; } else if (leadSelectedPieceOfFurniture == null || selectedFurniture.size() == 1 || selectedFurniture.indexOf(leadSelectedPieceOfFurniture) == -1) { leadSelectedPieceOfFurniture = selectedFurniture.get(0); } } }); // Add listener to update base plan lock when furniture movability changes final PropertyChangeListener furnitureChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent ev) { if (HomePieceOfFurniture.Property.MOVABLE.name().equals(ev.getPropertyName())) { // Remove non movable pieces from selection when base plan is locked HomePieceOfFurniture piece = (HomePieceOfFurniture)ev.getSource(); if (home.isBasePlanLocked() && isPieceOfFurniturePartOfBasePlan(piece)) { List<Selectable> selectedItems = home.getSelectedItems(); if (selectedItems.contains(piece)) { selectedItems = new ArrayList<Selectable>(selectedItems); selectedItems.remove(piece); home.setSelectedItems(selectedItems); } } } } }; for (HomePieceOfFurniture piece : home.getFurniture()) { piece.addPropertyChangeListener(furnitureChangeListener); } this.home.addFurnitureListener(new CollectionListener<HomePieceOfFurniture> () { public void collectionChanged(CollectionEvent<HomePieceOfFurniture> ev) { if (ev.getType() == CollectionEvent.Type.ADD) { ev.getItem().addPropertyChangeListener(furnitureChangeListener); } else if (ev.getType() == CollectionEvent.Type.DELETE) { ev.getItem().removePropertyChangeListener(furnitureChangeListener); } } }); } /** * Controls new furniture added to home. * Once added the furniture will be selected in view * and undo support will receive a new undoable edit. * @param furniture the furniture to add. */ public void addFurniture(List<HomePieceOfFurniture> furniture) { final boolean oldBasePlanLocked = this.home.isBasePlanLocked(); final List<Selectable> oldSelection = this.home.getSelectedItems(); final HomePieceOfFurniture [] newFurniture = furniture. toArray(new HomePieceOfFurniture [furniture.size()]); // Get indices of furniture added to home final int [] furnitureIndex = new int [furniture.size()]; int endIndex = this.home.getFurniture().size(); boolean basePlanLocked = oldBasePlanLocked; for (int i = 0; i < furnitureIndex.length; i++) { furnitureIndex [i] = endIndex++; // Unlock base plan if the piece is a part of it basePlanLocked &= !isPieceOfFurniturePartOfBasePlan(newFurniture [i]); } final boolean newBasePlanLocked = basePlanLocked; final Level furnitureLevel = this.home.getSelectedLevel(); doAddFurniture(newFurniture, furnitureIndex, furnitureLevel, null, newBasePlanLocked); if (this.undoSupport != null) { UndoableEdit undoableEdit = new AbstractUndoableEdit() { @Override public void undo() throws CannotUndoException { super.undo(); doDeleteFurniture(newFurniture, oldBasePlanLocked); home.setSelectedItems(oldSelection); } @Override public void redo() throws CannotRedoException { super.redo(); doAddFurniture(newFurniture, furnitureIndex, furnitureLevel, null, newBasePlanLocked); } @Override public String getPresentationName() { return preferences.getLocalizedString(FurnitureController.class, "undoAddFurnitureName"); } }; this.undoSupport.postEdit(undoableEdit); } } private void doAddFurniture(HomePieceOfFurniture [] furniture, int [] furnitureIndex, Level furnitureLevel, Level [] furnitureLevels, boolean basePlanLocked) { for (int i = 0; i < furnitureIndex.length; i++) { this.home.addPieceOfFurniture (furniture [i], furnitureIndex [i]); furniture [i].setLevel(furnitureLevels != null ? furnitureLevels [i] : furnitureLevel); } this.home.setBasePlanLocked(basePlanLocked); this.home.setSelectedItems(Arrays.asList(furniture)); } /** * Controls the deletion of the current selected furniture in home. * Once the selected furniture is deleted, undo support will receive a new undoable edit. */ public void deleteSelection() { deleteFurniture(Home.getFurnitureSubList(this.home.getSelectedItems())); } /** * Deletes the furniture of <code>deletedFurniture</code> from home. * Once the selected furniture is deleted, undo support will receive a new undoable edit. */ public void deleteFurniture(List<HomePieceOfFurniture> deletedFurniture) { final boolean basePlanLocked = this.home.isBasePlanLocked(); List<HomePieceOfFurniture> homeFurniture = this.home.getFurniture(); // Sort the deletable furniture in the ascending order of their index in home Map<Integer, HomePieceOfFurniture> sortedMap = new TreeMap<Integer, HomePieceOfFurniture>(); for (HomePieceOfFurniture piece : deletedFurniture) { if (isPieceOfFurnitureDeletable(piece)) { sortedMap.put(homeFurniture.indexOf(piece), piece); } } final HomePieceOfFurniture [] furniture = sortedMap.values(). toArray(new HomePieceOfFurniture [sortedMap.size()]); final int [] furnitureIndex = new int [furniture.length]; final Level [] furnitureLevels = new Level [furniture.length]; int i = 0; for (int index : sortedMap.keySet()) { furnitureIndex [i] = index; furnitureLevels [i] = furniture [i].getLevel(); i++; } doDeleteFurniture(furniture, basePlanLocked); if (this.undoSupport != null) { UndoableEdit undoableEdit = new AbstractUndoableEdit() { @Override public void undo() throws CannotUndoException { super.undo(); doAddFurniture(furniture, furnitureIndex, null, furnitureLevels, basePlanLocked); } @Override public void redo() throws CannotRedoException { super.redo(); home.setSelectedItems(Arrays.asList(furniture)); doDeleteFurniture(furniture, basePlanLocked); } @Override public String getPresentationName() { return preferences.getLocalizedString(FurnitureController.class, "undoDeleteSelectionName"); } }; this.undoSupport.postEdit(undoableEdit); } } private void doDeleteFurniture(HomePieceOfFurniture [] furniture, boolean basePlanLocked) { for (HomePieceOfFurniture piece : furniture) { this.home.deletePieceOfFurniture(piece); } this.home.setBasePlanLocked(basePlanLocked); } /** * Updates the selected furniture in home. */ public void setSelectedFurniture(List<HomePieceOfFurniture> selectedFurniture) { if (this.home.isBasePlanLocked()) { selectedFurniture = getFurnitureNotPartOfBasePlan(selectedFurniture); } this.home.setSelectedItems(selectedFurniture); } /** * Selects all furniture in home. */ public void selectAll() { setSelectedFurniture(this.home.getFurniture()); } /** * Returns <code>true</code> if the given <code>piece</code> is movable. */ protected boolean isPieceOfFurniturePartOfBasePlan(HomePieceOfFurniture piece) { return !piece.isMovable() || piece.isDoorOrWindow(); } /** * Returns <code>true</code> if the given <code>piece</code> may be moved. * Default implementation always returns <code>true</code>. */ protected boolean isPieceOfFurnitureMovable(HomePieceOfFurniture piece) { return true; } /** * Returns <code>true</code> if the given <code>piece</code> may be deleted. * Default implementation always returns <code>true</code>. */ protected boolean isPieceOfFurnitureDeletable(HomePieceOfFurniture piece) { return true; } /** * Returns a new home piece of furniture created from an other given <code>piece</code> of furniture. */ public HomePieceOfFurniture createHomePieceOfFurniture(PieceOfFurniture piece) { if (piece instanceof DoorOrWindow) { return new HomeDoorOrWindow((DoorOrWindow)piece); } else if (piece instanceof Light) { return new HomeLight((Light)piece); } else { return new HomePieceOfFurniture(piece); } } /** * Returns the furniture among the given list that are not part of the base plan. */ private List<HomePieceOfFurniture> getFurnitureNotPartOfBasePlan(List<HomePieceOfFurniture> furniture) { List<HomePieceOfFurniture> furnitureNotPartOfBasePlan = new ArrayList<HomePieceOfFurniture>(); for (HomePieceOfFurniture piece : furniture) { if (!isPieceOfFurniturePartOfBasePlan(piece)) { furnitureNotPartOfBasePlan.add(piece); } } return furnitureNotPartOfBasePlan; } /** * Uses <code>furnitureProperty</code> to sort home furniture * or cancels home furniture sort if home is already sorted on <code>furnitureProperty</code> * @param furnitureProperty a property of {@link HomePieceOfFurniture HomePieceOfFurniture} class. */ public void toggleFurnitureSort(HomePieceOfFurniture.SortableProperty furnitureProperty) { if (furnitureProperty.equals(this.home.getFurnitureSortedProperty())) { this.home.setFurnitureSortedProperty(null); } else { this.home.setFurnitureSortedProperty(furnitureProperty); } } /** * Toggles home furniture sort order. */ public void toggleFurnitureSortOrder() { this.home.setFurnitureDescendingSorted(!this.home.isFurnitureDescendingSorted()); } /** * Controls the sort of the furniture in home. If home furniture isn't sorted * or is sorted on an other property, it will be sorted on the given * <code>furnitureProperty</code> in ascending order. If home furniture is already * sorted on the given <code>furnitureProperty<code>, it will be sorted in descending * order, if the sort is in ascending order, otherwise it won't be sorted at all * and home furniture will be listed in insertion order. * @param furnitureProperty the furniture property on which the view wants * to sort the furniture it displays. */ public void sortFurniture(HomePieceOfFurniture.SortableProperty furnitureProperty) { // Compute sort algorithm described in javadoc final HomePieceOfFurniture.SortableProperty oldProperty = this.home.getFurnitureSortedProperty(); final boolean oldDescending = this.home.isFurnitureDescendingSorted(); boolean descending = false; if (furnitureProperty.equals(oldProperty)) { if (oldDescending) { furnitureProperty = null; } else { descending = true; } } this.home.setFurnitureSortedProperty(furnitureProperty); this.home.setFurnitureDescendingSorted(descending); } /** * Updates the furniture visible properties in home. */ public void setFurnitureVisibleProperties(List<HomePieceOfFurniture.SortableProperty> furnitureVisibleProperties) { this.home.setFurnitureVisibleProperties(furnitureVisibleProperties); } /** * Toggles furniture property visibility in home. */ public void toggleFurnitureVisibleProperty(HomePieceOfFurniture.SortableProperty furnitureProperty) { List<SortableProperty> furnitureVisibleProperties = new ArrayList<SortableProperty>(this.home.getFurnitureVisibleProperties()); if (furnitureVisibleProperties.contains(furnitureProperty)) { furnitureVisibleProperties.remove(furnitureProperty); // Ensure at least one column is visible if (furnitureVisibleProperties.isEmpty()) { furnitureVisibleProperties.add(HomePieceOfFurniture.SortableProperty.NAME); } } else { // Add furniture property after the visible property that has the previous index in // the following list List<HomePieceOfFurniture.SortableProperty> propertiesOrder = Arrays.asList(new HomePieceOfFurniture.SortableProperty [] { HomePieceOfFurniture.SortableProperty.CATALOG_ID, HomePieceOfFurniture.SortableProperty.NAME, HomePieceOfFurniture.SortableProperty.WIDTH, HomePieceOfFurniture.SortableProperty.DEPTH, HomePieceOfFurniture.SortableProperty.HEIGHT, HomePieceOfFurniture.SortableProperty.X, HomePieceOfFurniture.SortableProperty.Y, HomePieceOfFurniture.SortableProperty.ELEVATION, HomePieceOfFurniture.SortableProperty.ANGLE, HomePieceOfFurniture.SortableProperty.LEVEL, HomePieceOfFurniture.SortableProperty.COLOR, HomePieceOfFurniture.SortableProperty.TEXTURE, HomePieceOfFurniture.SortableProperty.MOVABLE, HomePieceOfFurniture.SortableProperty.DOOR_OR_WINDOW, HomePieceOfFurniture.SortableProperty.VISIBLE, HomePieceOfFurniture.SortableProperty.PRICE, HomePieceOfFurniture.SortableProperty.VALUE_ADDED_TAX_PERCENTAGE, HomePieceOfFurniture.SortableProperty.VALUE_ADDED_TAX, HomePieceOfFurniture.SortableProperty.PRICE_VALUE_ADDED_TAX_INCLUDED}); int propertyIndex = propertiesOrder.indexOf(furnitureProperty) - 1; if (propertyIndex > 0) { while (propertyIndex > 0) { int visiblePropertyIndex = furnitureVisibleProperties.indexOf(propertiesOrder.get(propertyIndex)); if (visiblePropertyIndex >= 0) { propertyIndex = visiblePropertyIndex + 1; break; } else { propertyIndex--; } } } if (propertyIndex < 0) { propertyIndex = 0; } furnitureVisibleProperties.add(propertyIndex, furnitureProperty); } this.home.setFurnitureVisibleProperties(furnitureVisibleProperties); } /** * Controls the modification of selected furniture. */ public void modifySelectedFurniture() { if (!Home.getFurnitureSubList(this.home.getSelectedItems()).isEmpty()) { new HomeFurnitureController(this.home, this.preferences, this.viewFactory, this.contentManager, this.undoSupport).displayView(getView()); } } /** * Controls the modification of the visibility of the selected piece of furniture. */ public void toggleSelectedFurnitureVisibility() { if (Home.getFurnitureSubList(this.home.getSelectedItems()).size() == 1) { HomeFurnitureController controller = new HomeFurnitureController(this.home, this.preferences, this.viewFactory, this.contentManager, this.undoSupport); controller.setVisible(!controller.getVisible()); controller.modifyFurniture(); } } /** * Groups the selected furniture as one piece of furniture. */ public void groupSelectedFurniture() { List<HomePieceOfFurniture> selectedFurniture = getMovableSelectedFurniture(); if (!selectedFurniture.isEmpty()) { final boolean basePlanLocked = this.home.isBasePlanLocked(); final List<Selectable> oldSelection = this.home.getSelectedItems(); List<HomePieceOfFurniture> homeFurniture = this.home.getFurniture(); // Sort the grouped furniture in the ascending order of their index in home Map<Integer, HomePieceOfFurniture> sortedMap = new TreeMap<Integer, HomePieceOfFurniture>(); for (HomePieceOfFurniture piece : selectedFurniture) { sortedMap.put(homeFurniture.indexOf(piece), piece); } final HomePieceOfFurniture [] groupPieces = sortedMap.values(). toArray(new HomePieceOfFurniture [sortedMap.size()]); final int [] groupPiecesIndex = new int [groupPieces.length]; final Level [] groupPiecesLevel = new Level [groupPieces.length]; final float [] groupPiecesElevation = new float [groupPieces.length]; final boolean [] groupPiecesMovable = new boolean [groupPieces.length]; final boolean [] groupPiecesVisible = new boolean [groupPieces.length]; Level minLevel = this.home.getSelectedLevel(); int i = 0; for (Entry<Integer, HomePieceOfFurniture> pieceEntry : sortedMap.entrySet()) { groupPiecesIndex [i] = pieceEntry.getKey(); HomePieceOfFurniture piece = pieceEntry.getValue(); groupPiecesLevel [i] = piece.getLevel(); groupPiecesElevation [i] = piece.getElevation(); groupPiecesMovable [i] = piece.isMovable(); groupPiecesVisible [i] = piece.isVisible(); if (groupPiecesLevel [i] != null) { if (minLevel == null || groupPiecesLevel [i].getElevation() < minLevel.getElevation()) { minLevel = groupPiecesLevel [i]; } } i++; } // Ensure that the lead piece is first to force the angle of the group on this piece int leadPieceIndex = selectedFurniture.indexOf(this.leadSelectedPieceOfFurniture); if (leadPieceIndex > 0) { selectedFurniture.remove(this.leadSelectedPieceOfFurniture); selectedFurniture.add(0, this.leadSelectedPieceOfFurniture); } final HomeFurnitureGroup furnitureGroup = createHomeFurnitureGroup(selectedFurniture); final float [] groupPiecesNewElevation = new float [groupPieces.length]; i = 0; for (HomePieceOfFurniture piece : sortedMap.values()) { groupPiecesNewElevation [i++] = piece.getElevation(); } final int furnitureGroupIndex = homeFurniture.size() - groupPieces.length; final boolean movable = furnitureGroup.isMovable(); final Level groupLevel = minLevel; doGroupFurniture(groupPieces, new HomeFurnitureGroup [] {furnitureGroup}, new int [] {furnitureGroupIndex}, new Level [] {groupLevel}, basePlanLocked); if (this.undoSupport != null) { UndoableEdit undoableEdit = new AbstractUndoableEdit() { @Override public void undo() throws CannotUndoException { super.undo(); doUngroupFurniture(groupPieces, groupPiecesIndex, new HomeFurnitureGroup [] {furnitureGroup}, groupPiecesLevel, basePlanLocked); for (int i = 0; i < groupPieces.length; i++) { groupPieces [i].setElevation(groupPiecesElevation [i]); groupPieces [i].setMovable(groupPiecesMovable [i]); groupPieces [i].setVisible(groupPiecesVisible [i]); } home.setSelectedItems(oldSelection); } @Override public void redo() throws CannotRedoException { super.redo(); for (int i = 0; i < groupPieces.length; i++) { groupPieces [i].setElevation(groupPiecesNewElevation [i]); groupPieces [i].setLevel(null); } furnitureGroup.setMovable(movable); furnitureGroup.setVisible(true); doGroupFurniture(groupPieces, new HomeFurnitureGroup [] {furnitureGroup}, new int [] {furnitureGroupIndex}, new Level [] {groupLevel}, basePlanLocked); } @Override public String getPresentationName() { return preferences.getLocalizedString(FurnitureController.class, "undoGroupName"); } }; this.undoSupport.postEdit(undoableEdit); } } } /** * Returns a new furniture group for the given furniture list. */ protected HomeFurnitureGroup createHomeFurnitureGroup(List<HomePieceOfFurniture> furniture) { String furnitureGroupName = this.preferences.getLocalizedString( FurnitureController.class, "groupName", getFurnitureGroupCount(this.home.getFurniture()) + 1); final HomeFurnitureGroup furnitureGroup = new HomeFurnitureGroup(furniture, furnitureGroupName); return furnitureGroup; } /** * Returns the count of furniture groups among the given list. */ private int getFurnitureGroupCount(List<HomePieceOfFurniture> furniture) { int i = 0; for (HomePieceOfFurniture piece : furniture) { if (piece instanceof HomeFurnitureGroup) { i += 1 + getFurnitureGroupCount(((HomeFurnitureGroup)piece).getFurniture()); } } return i; } private void doGroupFurniture(HomePieceOfFurniture [] groupPieces, HomeFurnitureGroup [] furnitureGroups, int [] furnitureGroupsIndex, Level [] groupLevels, boolean basePlanLocked) { doDeleteFurniture(groupPieces, basePlanLocked); doAddFurniture(furnitureGroups, furnitureGroupsIndex, null, groupLevels, basePlanLocked); } private void doUngroupFurniture(HomePieceOfFurniture [] groupPieces, int [] groupPiecesIndex, HomeFurnitureGroup [] furnitureGroups, Level [] groupLevels, boolean basePlanLocked) { doDeleteFurniture(furnitureGroups, basePlanLocked); doAddFurniture(groupPieces, groupPiecesIndex, null, groupLevels, basePlanLocked); } /** * Ungroups the selected groups of furniture. */ public void ungroupSelectedFurniture() { List<HomeFurnitureGroup> movableSelectedFurnitureGroups = new ArrayList<HomeFurnitureGroup>(); for (Selectable item : this.home.getSelectedItems()) { if (item instanceof HomeFurnitureGroup) { HomeFurnitureGroup group = (HomeFurnitureGroup)item; if (isPieceOfFurnitureMovable(group)) { movableSelectedFurnitureGroups.add(group); } } } if (!movableSelectedFurnitureGroups.isEmpty()) { final boolean oldBasePlanLocked = this.home.isBasePlanLocked(); final List<Selectable> oldSelection = this.home.getSelectedItems(); List<HomePieceOfFurniture> homeFurniture = this.home.getFurniture(); // Sort the groups in the ascending order of their index in home Map<Integer, HomeFurnitureGroup> sortedMap = new TreeMap<Integer, HomeFurnitureGroup>(); for (HomeFurnitureGroup group : movableSelectedFurnitureGroups) { sortedMap.put(homeFurniture.indexOf(group), group); } final HomeFurnitureGroup [] furnitureGroups = sortedMap.values(). toArray(new HomeFurnitureGroup [sortedMap.size()]); final int [] furnitureGroupsIndex = new int [furnitureGroups.length]; int i = 0; for (int index : sortedMap.keySet()) { furnitureGroupsIndex [i++] = index; } List<HomePieceOfFurniture> groupPiecesList = new ArrayList<HomePieceOfFurniture>(); for (HomeFurnitureGroup furnitureGroup : furnitureGroups) { groupPiecesList.addAll(furnitureGroup.getFurniture()); } final HomePieceOfFurniture [] groupPieces = groupPiecesList.toArray(new HomePieceOfFurniture [groupPiecesList.size()]); final int [] groupPiecesIndex = new int [groupPieces.length]; final Level [] groupPiecesLevel = new Level [groupPieces.length]; int endIndex = homeFurniture.size() - furnitureGroups.length; boolean basePlanLocked = oldBasePlanLocked; for (i = 0; i < groupPieces.length; i++) { groupPiecesIndex [i] = endIndex++; groupPiecesLevel [i] = groupPieces [i].getLevel(); // Unlock base plan if the piece is a part of it basePlanLocked &= !isPieceOfFurniturePartOfBasePlan(groupPieces [i]); } final boolean newBasePlanLocked = basePlanLocked; doUngroupFurniture(groupPieces, groupPiecesIndex, furnitureGroups, groupPiecesLevel, newBasePlanLocked); if (this.undoSupport != null) { UndoableEdit undoableEdit = new AbstractUndoableEdit() { @Override public void undo() throws CannotUndoException { super.undo(); doGroupFurniture(groupPieces, furnitureGroups, furnitureGroupsIndex, groupPiecesLevel, oldBasePlanLocked); home.setSelectedItems(oldSelection); } @Override public void redo() throws CannotRedoException { super.redo(); doUngroupFurniture(groupPieces, groupPiecesIndex, furnitureGroups, groupPiecesLevel, newBasePlanLocked); } @Override public String getPresentationName() { return preferences.getLocalizedString(FurnitureController.class, "undoUngroupName"); } }; this.undoSupport.postEdit(undoableEdit); } } } /** * Displays the wizard that helps to import furniture to home. */ public void importFurniture() { new ImportedFurnitureWizardController(this.home, this.preferences, this, this.viewFactory, this.contentManager, this.undoSupport).displayView(getView()); } /** * Displays the wizard that helps to import furniture to home with a * given model name. */ public void importFurniture(String modelName) { new ImportedFurnitureWizardController(this.home, modelName, this.preferences, this, this.viewFactory, this.contentManager, this.undoSupport).displayView(getView()); } /** * Controls the alignment of selected furniture on top of the first selected piece. */ public void alignSelectedFurnitureOnTop() { alignSelectedFurniture(new AlignmentAction() { public void alignFurniture(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece) { float minYLeadPiece = getMinY(leadPiece); for (AlignedPieceOfFurniture alignedPiece : alignedFurniture) { HomePieceOfFurniture piece = alignedPiece.getPieceOfFurniture(); float minY = getMinY(piece); piece.setY(piece.getY() + minYLeadPiece - minY); } } }); } /** * Controls the alignment of selected furniture on bottom of the first selected piece. */ public void alignSelectedFurnitureOnBottom() { alignSelectedFurniture(new AlignmentAction() { public void alignFurniture(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece) { float maxYLeadPiece = getMaxY(leadPiece); for (AlignedPieceOfFurniture alignedPiece : alignedFurniture) { HomePieceOfFurniture piece = alignedPiece.getPieceOfFurniture(); float maxY = getMaxY(piece); piece.setY(piece.getY() + maxYLeadPiece - maxY); } } }); } /** * Controls the alignment of selected furniture on left of the first selected piece. */ public void alignSelectedFurnitureOnLeft() { alignSelectedFurniture(new AlignmentAction() { public void alignFurniture(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece) { float minXLeadPiece = getMinX(leadPiece); for (AlignedPieceOfFurniture alignedPiece : alignedFurniture) { HomePieceOfFurniture piece = alignedPiece.getPieceOfFurniture(); float minX = getMinX(piece); piece.setX(piece.getX() + minXLeadPiece - minX); } } }); } /** * Controls the alignment of selected furniture on right of the first selected piece. */ public void alignSelectedFurnitureOnRight() { alignSelectedFurniture(new AlignmentAction() { public void alignFurniture(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece) { float maxXLeadPiece = getMaxX(leadPiece); for (AlignedPieceOfFurniture alignedPiece : alignedFurniture) { HomePieceOfFurniture piece = alignedPiece.getPieceOfFurniture(); float maxX = getMaxX(piece); piece.setX(piece.getX() + maxXLeadPiece - maxX); } } }); } /** * Controls the alignment of selected furniture on the front side of the first selected piece. */ public void alignSelectedFurnitureOnFrontSide() { alignSelectedFurniture(new AlignmentAction() { public void alignFurniture(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece) { float [][] points = leadPiece.getPoints(); Line2D frontLine = new Line2D.Float(points [2][0], points [2][1], points [3][0], points [3][1]); for (AlignedPieceOfFurniture alignedPiece : alignedFurniture) { alignPieceOfFurnitureAlongSides(alignedPiece.getPieceOfFurniture(), leadPiece, frontLine, true, null, 0); } } }); } /** * Controls the alignment of selected furniture on the back side of the first selected piece. */ public void alignSelectedFurnitureOnBackSide() { alignSelectedFurniture(new AlignmentAction() { public void alignFurniture(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece) { float [][] points = leadPiece.getPoints(); Line2D backLine = new Line2D.Float(points [0][0], points [0][1], points [1][0], points [1][1]); for (AlignedPieceOfFurniture alignedPiece : alignedFurniture) { alignPieceOfFurnitureAlongSides(alignedPiece.getPieceOfFurniture(), leadPiece, backLine, false, null, 0); } } }); } /** * Controls the alignment of selected furniture on the left side of the first selected piece. */ public void alignSelectedFurnitureOnLeftSide() { alignSelectedFurniture(new AlignmentAction() { public void alignFurniture(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece) { float [][] points = leadPiece.getPoints(); Line2D leftLine = new Line2D.Float(points [3][0], points [3][1], points [0][0], points [0][1]); for (AlignedPieceOfFurniture alignedPiece : alignedFurniture) { alignPieceOfFurnitureAlongLeftOrRightSides(alignedPiece.getPieceOfFurniture(), leadPiece, leftLine, false); } } }); } /** * Controls the alignment of selected furniture on the right side of the first selected piece. */ public void alignSelectedFurnitureOnRightSide() { alignSelectedFurniture(new AlignmentAction() { public void alignFurniture(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece) { float [][] points = leadPiece.getPoints(); Line2D rightLine = new Line2D.Float(points [1][0], points [1][1], points [2][0], points [2][1]); for (AlignedPieceOfFurniture alignedPiece : alignedFurniture) { alignPieceOfFurnitureAlongLeftOrRightSides(alignedPiece.getPieceOfFurniture(), leadPiece, rightLine, true); } } }); } /** * Controls the alignment of selected furniture on the sides of the first selected piece. */ public void alignSelectedFurnitureSideBySide() { alignSelectedFurniture(new AlignmentAction() { public void alignFurniture(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece) { alignFurnitureSideBySide(alignedFurniture, leadPiece); } }); } private void alignFurnitureSideBySide(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece) { float [][] points = leadPiece.getPoints(); final Line2D centerLine = new Line2D.Float(leadPiece.getX(), leadPiece.getY(), (points [0][0] + points [1][0]) / 2, (points [0][1] + points [1][1]) / 2); List<HomePieceOfFurniture> furnitureSortedAlongBackLine = sortFurniture(alignedFurniture, leadPiece, centerLine); int leadPieceIndex = furnitureSortedAlongBackLine.indexOf(leadPiece); Line2D backLine = new Line2D.Float(points [0][0], points [0][1], points [1][0], points [1][1]); float sideDistance = leadPiece.getWidth() / 2; for (int i = leadPieceIndex + 1; i < furnitureSortedAlongBackLine.size(); i++) { sideDistance += alignPieceOfFurnitureAlongSides(furnitureSortedAlongBackLine.get(i), leadPiece, backLine, false, centerLine, sideDistance); } sideDistance = -leadPiece.getWidth() / 2; for (int i = leadPieceIndex - 1; i >= 0; i--) { sideDistance -= alignPieceOfFurnitureAlongSides(furnitureSortedAlongBackLine.get(i), leadPiece, backLine, false, centerLine, sideDistance); } } /** * Returns a list containing aligned furniture and lead piece sorted in the order of their distribution along * a line orthogonal to the given axis. */ public List<HomePieceOfFurniture> sortFurniture(AlignedPieceOfFurniture [] furniture, HomePieceOfFurniture leadPiece, final Line2D orthogonalAxis) { List<HomePieceOfFurniture> sortedFurniture = new ArrayList<HomePieceOfFurniture>(furniture.length + 1); if (leadPiece != null) { sortedFurniture.add(leadPiece); } for (AlignedPieceOfFurniture piece : furniture) { sortedFurniture.add(piece.getPieceOfFurniture()); } Collections.sort(sortedFurniture, new Comparator<HomePieceOfFurniture>() { public int compare(HomePieceOfFurniture p1, HomePieceOfFurniture p2) { return Double.compare(orthogonalAxis.ptLineDistSq(p2.getX(), p2.getY()) * orthogonalAxis.relativeCCW(p2.getX(), p2.getY()), orthogonalAxis.ptLineDistSq(p1.getX(), p1.getY()) * orthogonalAxis.relativeCCW(p1.getX(), p1.getY())); } }); return sortedFurniture; } /** * Aligns the given <code>piece</code> along the front or back side of the lead piece and its left or right side * at a distance equal to <code>sideDistance</code>, and returns the width of the bounding box of * the <code>piece</code> along the back side axis. */ private double alignPieceOfFurnitureAlongSides(HomePieceOfFurniture piece, HomePieceOfFurniture leadPiece, Line2D frontOrBackLine, boolean frontLine, Line2D centerLine, float sideDistance) { // Search the distance required to align piece on the front or back side double distance = frontOrBackLine.relativeCCW(piece.getX(), piece.getY()) * frontOrBackLine.ptLineDist(piece.getX(), piece.getY()) + getPieceBoundingRectangleHeight(piece, -leadPiece.getAngle()) / 2; if (frontLine) { distance = -distance; } double sinLeadPieceAngle = Math.sin(leadPiece.getAngle()); double cosLeadPieceAngle = Math.cos(leadPiece.getAngle()); float deltaX = (float)(-distance * sinLeadPieceAngle); float deltaY = (float)(distance * cosLeadPieceAngle); double rotatedBoundingBoxWidth = getPieceBoundingRectangleWidth(piece, -leadPiece.getAngle()); if (centerLine != null) { // Search the distance required to align piece on the side of the previous piece distance = sideDistance + centerLine.relativeCCW(piece.getX(), piece.getY()) * (centerLine.ptLineDist(piece.getX(), piece.getY()) - rotatedBoundingBoxWidth / 2); deltaX += (float)(distance * cosLeadPieceAngle); deltaY += (float)(distance * sinLeadPieceAngle); } piece.move(deltaX, deltaY); return rotatedBoundingBoxWidth; } /** * Aligns the given <code>piece</code> along the left or right side of the lead piece. */ private void alignPieceOfFurnitureAlongLeftOrRightSides(HomePieceOfFurniture piece, HomePieceOfFurniture leadPiece, Line2D leftOrRightLine, boolean rightLine) { // Search the distance required to align piece on the side of the lead piece double distance = leftOrRightLine.relativeCCW(piece.getX(), piece.getY()) * leftOrRightLine.ptLineDist(piece.getX(), piece.getY()) + getPieceBoundingRectangleWidth(piece, -leadPiece.getAngle()) / 2; if (rightLine) { distance = -distance; } piece.move((float)(distance * Math.cos(leadPiece.getAngle())), (float)(distance * Math.sin(leadPiece.getAngle()))); } /** * Returns the bounding box width of the given piece when it's rotated of an additional angle. */ private double getPieceBoundingRectangleWidth(HomePieceOfFurniture piece, float additionalAngle) { return Math.abs(piece.getWidth() * Math.cos(additionalAngle + piece.getAngle())) + Math.abs(piece.getDepth() * Math.sin(additionalAngle + piece.getAngle())); } /** * Returns the bounding box height of the given piece when it's rotated of an additional angle. */ private double getPieceBoundingRectangleHeight(HomePieceOfFurniture piece, float additionalAngle) { return Math.abs(piece.getWidth() * Math.sin(additionalAngle + piece.getAngle())) + Math.abs(piece.getDepth() * Math.cos(additionalAngle + piece.getAngle())); } /** * Controls the alignment of selected furniture. */ private void alignSelectedFurniture(final AlignmentAction alignmentAction) { final List<HomePieceOfFurniture> selectedFurniture = getMovableSelectedFurniture(); if (selectedFurniture.size() >= 2) { final List<Selectable> oldSelection = this.home.getSelectedItems(); final HomePieceOfFurniture leadPiece = this.leadSelectedPieceOfFurniture; final AlignedPieceOfFurniture [] alignedFurniture = AlignedPieceOfFurniture.getAlignedFurniture(selectedFurniture, leadPiece); this.home.setSelectedItems(selectedFurniture); alignmentAction.alignFurniture(alignedFurniture, leadPiece); if (this.undoSupport != null) { UndoableEdit undoableEdit = new AbstractUndoableEdit() { @Override public void undo() throws CannotUndoException { super.undo(); undoAlignFurniture(alignedFurniture); home.setSelectedItems(oldSelection); } @Override public void redo() throws CannotRedoException { super.redo(); home.setSelectedItems(selectedFurniture); alignmentAction.alignFurniture(alignedFurniture, leadPiece); } @Override public String getPresentationName() { return preferences.getLocalizedString(FurnitureController.class, "undoAlignName"); } }; this.undoSupport.postEdit(undoableEdit); } } } private List<HomePieceOfFurniture> getMovableSelectedFurniture() { List<HomePieceOfFurniture> movableSelectedFurniture = new ArrayList<HomePieceOfFurniture>(); for (Selectable item : this.home.getSelectedItems()) { if (item instanceof HomePieceOfFurniture) { HomePieceOfFurniture piece = (HomePieceOfFurniture)item; if (isPieceOfFurnitureMovable(piece)) { movableSelectedFurniture.add(piece); } } } return movableSelectedFurniture; } private void undoAlignFurniture(AlignedPieceOfFurniture [] alignedFurniture) { for (AlignedPieceOfFurniture alignedPiece : alignedFurniture) { HomePieceOfFurniture piece = alignedPiece.getPieceOfFurniture(); piece.setX(alignedPiece.getX()); piece.setY(alignedPiece.getY()); } } /** * Returns the minimum abscissa of the vertices of <code>piece</code>. */ private float getMinX(HomePieceOfFurniture piece) { float [][] points = piece.getPoints(); float minX = Float.POSITIVE_INFINITY; for (float [] point : points) { minX = Math.min(minX, point [0]); } return minX; } /** * Returns the maximum abcissa of the vertices of <code>piece</code>. */ private float getMaxX(HomePieceOfFurniture piece) { float [][] points = piece.getPoints(); float maxX = Float.NEGATIVE_INFINITY; for (float [] point : points) { maxX = Math.max(maxX, point [0]); } return maxX; } /** * Returns the minimum ordinate of the vertices of <code>piece</code>. */ private float getMinY(HomePieceOfFurniture piece) { float [][] points = piece.getPoints(); float minY = Float.POSITIVE_INFINITY; for (float [] point : points) { minY = Math.min(minY, point [1]); } return minY; } /** * Returns the maximum ordinate of the vertices of <code>piece</code>. */ private float getMaxY(HomePieceOfFurniture piece) { float [][] points = piece.getPoints(); float maxY = Float.NEGATIVE_INFINITY; for (float [] point : points) { maxY = Math.max(maxY, point [1]); } return maxY; } /** * Controls the distribution of the selected furniture along horizontal axis. */ public void distributeSelectedFurnitureHorizontally() { distributeSelectedFurniture(true); } /** * Controls the distribution of the selected furniture along vertical axis. */ public void distributeSelectedFurnitureVertically() { distributeSelectedFurniture(false); } /** * Controls the distribution of the selected furniture along the axis orthogonal to the given one. */ public void distributeSelectedFurniture(final boolean horizontal) { final List<HomePieceOfFurniture> selectedFurniture = getMovableSelectedFurniture(); if (selectedFurniture.size() >= 3) { final List<Selectable> oldSelection = this.home.getSelectedItems(); final AlignedPieceOfFurniture [] alignedFurniture = AlignedPieceOfFurniture.getAlignedFurniture(selectedFurniture, null); this.home.setSelectedItems(selectedFurniture); doDistributeFurnitureAlongAxis(alignedFurniture, horizontal); if (this.undoSupport != null) { UndoableEdit undoableEdit = new AbstractUndoableEdit() { @Override public void undo() throws CannotUndoException { super.undo(); undoAlignFurniture(alignedFurniture); home.setSelectedItems(oldSelection); } @Override public void redo() throws CannotRedoException { super.redo(); home.setSelectedItems(selectedFurniture); doDistributeFurnitureAlongAxis(alignedFurniture, horizontal); } @Override public String getPresentationName() { return preferences.getLocalizedString(FurnitureController.class, "undoDistributeName"); } }; this.undoSupport.postEdit(undoableEdit); } } } private void doDistributeFurnitureAlongAxis(AlignedPieceOfFurniture [] alignedFurniture, boolean horizontal) { Line2D orthogonalAxis = horizontal ? new Line2D.Float(0, 0, 0, -1) : new Line2D.Float(0, 0, 1, 0); List<HomePieceOfFurniture> furnitureHorizontallySorted = sortFurniture(alignedFurniture, null, orthogonalAxis); float axisAngle = (float)(horizontal ? 0 : Math.PI / 2); HomePieceOfFurniture firstPiece = furnitureHorizontallySorted.get(0); double firstPieceBoundingRectangleHalfWidth = getPieceBoundingRectangleWidth(firstPiece, axisAngle) / 2; HomePieceOfFurniture lastPiece = furnitureHorizontallySorted.get(furnitureHorizontallySorted.size() - 1); double lastPieceBoundingRectangleHalfWidth = getPieceBoundingRectangleWidth(lastPiece, axisAngle) / 2; double gap = Math.abs(orthogonalAxis.ptLineDist(lastPiece.getX(), lastPiece.getY()) * orthogonalAxis.relativeCCW(lastPiece.getX(), lastPiece.getY()) - orthogonalAxis.ptLineDist(firstPiece.getX(), firstPiece.getY()) * orthogonalAxis.relativeCCW(firstPiece.getX(), firstPiece.getY())) - lastPieceBoundingRectangleHalfWidth - firstPieceBoundingRectangleHalfWidth; double [] furnitureWidthsAlongAxis = new double [furnitureHorizontallySorted.size() - 2]; for (int i = 1; i < furnitureHorizontallySorted.size() - 1; i++) { HomePieceOfFurniture piece = furnitureHorizontallySorted.get(i); furnitureWidthsAlongAxis [i - 1] = getPieceBoundingRectangleWidth(piece, axisAngle); gap -= furnitureWidthsAlongAxis [i - 1]; } gap /= furnitureHorizontallySorted.size() - 1; float xOrY = (horizontal ? firstPiece.getX() : firstPiece.getY()) + (float)(firstPieceBoundingRectangleHalfWidth + gap); for (int i = 1; i < furnitureHorizontallySorted.size() - 1; i++) { HomePieceOfFurniture piece = furnitureHorizontallySorted.get(i); if (horizontal) { piece.setX((float)(xOrY + furnitureWidthsAlongAxis [i - 1] / 2)); } else { piece.setY((float)(xOrY + furnitureWidthsAlongAxis [i - 1] / 2)); } xOrY += gap + furnitureWidthsAlongAxis [i - 1]; } } /** * Stores the current x or y value of an aligned piece of furniture. */ private static class AlignedPieceOfFurniture { private HomePieceOfFurniture piece; private float x; private float y; public AlignedPieceOfFurniture(HomePieceOfFurniture piece) { this.piece = piece; this.x = piece.getX(); this.y = piece.getY(); } public HomePieceOfFurniture getPieceOfFurniture() { return this.piece; } public float getX() { return this.x; } public float getY() { return this.y; } /** * A helper method that returns an array of <code>AlignedPieceOfFurniture</code> * built from <code>furniture</code> pieces excepted for <code>leadPiece</code>. */ public static AlignedPieceOfFurniture [] getAlignedFurniture(List<HomePieceOfFurniture> furniture, HomePieceOfFurniture leadPiece) { final AlignedPieceOfFurniture[] alignedFurniture = new AlignedPieceOfFurniture[leadPiece == null ? furniture.size() : furniture.size() - 1]; int i = 0; for (HomePieceOfFurniture piece : furniture) { if (piece != leadPiece) { alignedFurniture[i++] = new AlignedPieceOfFurniture(piece); } } return alignedFurniture; } } /** * Describes how to align furniture on a lead piece. */ private static interface AlignmentAction { public void alignFurniture(AlignedPieceOfFurniture [] alignedFurniture, HomePieceOfFurniture leadPiece); } }