All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.eteks.sweethome3d.viewcontroller.PlanController Maven / Gradle / Ivy

/*
 * PlanController.java 2 juin 2006
 *
 * Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks 
 *
 * 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.BasicStroke;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.BackgroundImage;
import com.eteks.sweethome3d.model.Baseboard;
import com.eteks.sweethome3d.model.Camera;
import com.eteks.sweethome3d.model.CollectionEvent;
import com.eteks.sweethome3d.model.CollectionListener;
import com.eteks.sweethome3d.model.Compass;
import com.eteks.sweethome3d.model.DimensionLine;
import com.eteks.sweethome3d.model.Elevatable;
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.HomeTexture;
import com.eteks.sweethome3d.model.Label;
import com.eteks.sweethome3d.model.LengthUnit;
import com.eteks.sweethome3d.model.Level;
import com.eteks.sweethome3d.model.ObserverCamera;
import com.eteks.sweethome3d.model.Polyline;
import com.eteks.sweethome3d.model.Room;
import com.eteks.sweethome3d.model.Selectable;
import com.eteks.sweethome3d.model.SelectionEvent;
import com.eteks.sweethome3d.model.SelectionListener;
import com.eteks.sweethome3d.model.TextStyle;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.model.Wall;

/**
 * A MVC controller for the plan view.
 * @author Emmanuel Puybaret
 */
public class PlanController extends FurnitureController implements Controller {
  public enum Property {MODE, MODIFICATION_STATE, BASE_PLAN_MODIFICATION_STATE, SCALE}

  /**
   * Selectable modes in controller.
   */
  public static class Mode {
    // Don't qualify Mode as an enumeration to be able to extend Mode class
    public static final Mode SELECTION               = new Mode("SELECTION");
    public static final Mode PANNING                 = new Mode("PANNING");
    public static final Mode WALL_CREATION           = new Mode("WALL_CREATION");
    public static final Mode ROOM_CREATION           = new Mode("ROOM_CREATION");
    public static final Mode POLYLINE_CREATION       = new Mode("POLYLINE_CREATION");
    public static final Mode DIMENSION_LINE_CREATION = new Mode("DIMENSION_LINE_CREATION");
    public static final Mode LABEL_CREATION          = new Mode("LABEL_CREATION");
    
    private final String name;
    
    protected Mode(String name) {
      this.name = name;      
    }
    
    public final String name() {
      return this.name;
    }
    
    @Override
    public String toString() {
      return this.name;
    }
  };

  /**
   * Fields that can be edited in plan view.
   */
  public static enum EditableProperty {X, Y, LENGTH, ANGLE, THICKNESS, OFFSET, ARC_EXTENT}

  private static final String SCALE_VISUAL_PROPERTY = "com.eteks.sweethome3d.SweetHome3D.PlanScale";
  
  private static final int PIXEL_MARGIN           = 4;
  private static final int INDICATOR_PIXEL_MARGIN = 5;
  private static final int WALL_ENDS_PIXEL_MARGIN = 2;

  private final Home                  home;
  private final UserPreferences       preferences;
  private final ViewFactory           viewFactory;
  private final ContentManager        contentManager;
  private final UndoableEditSupport   undoSupport;
  private final PropertyChangeSupport propertyChangeSupport;
  private PlanView                    planView;
  private SelectionListener           selectionListener;
  private PropertyChangeListener      wallChangeListener;
  // Possibles states
  private final ControllerState       selectionState;
  private final ControllerState       rectangleSelectionState;
  private final ControllerState       selectionMoveState;
  private final ControllerState       panningState;
  private final ControllerState       dragAndDropState;
  private final ControllerState       wallCreationState;
  private final ControllerState       wallDrawingState;
  private final ControllerState       wallResizeState;
  private final ControllerState       pieceOfFurnitureRotationState;
  private final ControllerState       pieceOfFurnitureElevationState;
  private final ControllerState       pieceOfFurnitureHeightState;
  private final ControllerState       pieceOfFurnitureResizeState;
  private final ControllerState       lightPowerModificationState;
  private final ControllerState       pieceOfFurnitureNameOffsetState;
  private final ControllerState       pieceOfFurnitureNameRotationState;
  private final ControllerState       cameraYawRotationState;
  private final ControllerState       cameraPitchRotationState;
  private final ControllerState       cameraElevationState;
  private final ControllerState       dimensionLineCreationState;
  private final ControllerState       dimensionLineDrawingState;
  private final ControllerState       dimensionLineResizeState;
  private final ControllerState       dimensionLineOffsetState;
  private final ControllerState       roomCreationState;
  private final ControllerState       roomDrawingState;
  private final ControllerState       roomResizeState;
  private final ControllerState       roomAreaOffsetState;
  private final ControllerState       roomAreaRotationState;
  private final ControllerState       roomNameOffsetState;
  private final ControllerState       roomNameRotationState;
  private final ControllerState       polylineCreationState;
  private final ControllerState       polylineDrawingState;
  private final ControllerState       polylineResizeState;
  private final ControllerState       labelCreationState;
  private final ControllerState       labelRotationState;
  private final ControllerState       labelElevationState;
  private final ControllerState       compassRotationState;
  private final ControllerState       compassResizeState;
  // Current state
  private ControllerState             state;
  private ControllerState             previousState;
  // Mouse cursor position at last mouse press
  private float                           xLastMousePress;
  private float                           yLastMousePress;
  private boolean                         shiftDownLastMousePress;
  private boolean                         alignmentActivatedLastMousePress;
  private boolean                         duplicationActivatedLastMousePress;
  private boolean                         magnetismToggledLastMousePress;
  private float                           xLastMouseMove;
  private float                           yLastMouseMove;
  private Area                            wallsAreaCache;
  private Area                            wallsIncludingBaseboardsAreaCache;
  private Area                            insideWallsAreaCache;
  private List               roomPathsCache;
  private Map furnitureSidesCache;
  private List                draggedItems;

  /**
   * Creates the controller of plan view. 
   * @param home        the home plan edited by this controller and its view
   * @param preferences the preferences of the application
   * @param viewFactory a factory able to create the plan view managed by this controller
   * @param contentManager a content manager used to import furniture
   * @param undoSupport undo support to post changes on plan by this controller
   */
  public PlanController(Home home, 
                        UserPreferences preferences, 
                        ViewFactory viewFactory,
                        ContentManager contentManager,
                        UndoableEditSupport undoSupport) {
    super(home, preferences, viewFactory, contentManager, undoSupport);
    this.home = home;
    this.preferences = preferences;
    this.viewFactory = viewFactory;
    this.contentManager = contentManager;
    this.undoSupport = undoSupport;
    this.propertyChangeSupport = new PropertyChangeSupport(this);
    this.furnitureSidesCache = new Hashtable();
    // Initialize states
    this.selectionState = new SelectionState();
    this.selectionMoveState = new SelectionMoveState();
    this.rectangleSelectionState = new RectangleSelectionState();
    this.panningState = new PanningState();
    this.dragAndDropState = new DragAndDropState();
    this.wallCreationState = new WallCreationState();
    this.wallDrawingState = new WallDrawingState();
    this.wallResizeState = new WallResizeState();
    this.pieceOfFurnitureRotationState = new PieceOfFurnitureRotationState();
    this.pieceOfFurnitureElevationState = new PieceOfFurnitureElevationState();
    this.pieceOfFurnitureHeightState = new PieceOfFurnitureHeightState();
    this.pieceOfFurnitureResizeState = new PieceOfFurnitureResizeState();
    this.lightPowerModificationState = new LightPowerModificationState();
    this.pieceOfFurnitureNameOffsetState = new PieceOfFurnitureNameOffsetState();
    this.pieceOfFurnitureNameRotationState = new PieceOfFurnitureNameRotationState();
    this.cameraYawRotationState = new CameraYawRotationState();
    this.cameraPitchRotationState = new CameraPitchRotationState();
    this.cameraElevationState = new CameraElevationState();
    this.dimensionLineCreationState = new DimensionLineCreationState();
    this.dimensionLineDrawingState = new DimensionLineDrawingState();
    this.dimensionLineResizeState = new DimensionLineResizeState();
    this.dimensionLineOffsetState = new DimensionLineOffsetState();
    this.roomCreationState = new RoomCreationState();
    this.roomDrawingState = new RoomDrawingState();
    this.roomResizeState = new RoomResizeState();
    this.roomAreaOffsetState = new RoomAreaOffsetState();
    this.roomAreaRotationState = new RoomAreaRotationState();
    this.roomNameOffsetState = new RoomNameOffsetState();
    this.roomNameRotationState = new RoomNameRotationState();
    this.polylineCreationState = new PolylineCreationState();
    this.polylineDrawingState = new PolylineDrawingState();
    this.polylineResizeState = new PolylineResizeState();
    this.labelCreationState = new LabelCreationState();
    this.labelRotationState = new LabelRotationState();
    this.labelElevationState = new LabelElevationState();
    this.compassRotationState = new CompassRotationState();
    this.compassResizeState = new CompassResizeState();
    // Set default state to selectionState
    setState(this.selectionState);
    
    addModelListeners();
    
    // Restore previous scale if it exists
    Number scale = home.getNumericProperty(SCALE_VISUAL_PROPERTY);
    if (scale != null) {
      setScale(scale.floatValue());
    }
  }

  /**
   * Returns the view associated with this controller.
   */
  public PlanView getView() {
    // Create view lazily only once it's needed
    if (this.planView == null) {
      this.planView = this.viewFactory.createPlanView(this.home, this.preferences, this);
    }
    return this.planView;
  }

  /**
   * Changes current state of controller.
   */
  protected void setState(ControllerState state) {
    boolean oldModificationState = false;
    boolean oldBasePlanModificationState = false;
    if (this.state != null) {
      oldModificationState = this.state.isModificationState();
      oldBasePlanModificationState = this.state.isBasePlanModificationState();
      this.state.exit();
    }
    
    this.previousState = this.state;
    this.state = state;
    this.state.enter();
    
    if (oldModificationState != state.isModificationState()) {
      this.propertyChangeSupport.firePropertyChange(Property.MODIFICATION_STATE.name(), 
          oldModificationState, !oldModificationState);
    }
    if (oldBasePlanModificationState != state.isBasePlanModificationState()) {
      this.propertyChangeSupport.firePropertyChange(Property.BASE_PLAN_MODIFICATION_STATE.name(), 
          oldBasePlanModificationState, !oldBasePlanModificationState);
    }
  }
  
  /**
   * Adds the property change listener in parameter to this controller.
   */
  public void addPropertyChangeListener(Property property, PropertyChangeListener listener) {
    this.propertyChangeSupport.addPropertyChangeListener(property.name(), listener);
  }

  /**
   * Removes the property change listener in parameter from this controller.
   */
  public void removePropertyChangeListener(Property property, PropertyChangeListener listener) {
    this.propertyChangeSupport.removePropertyChangeListener(property.name(), listener);
  }

  /**
   * Returns the active mode of this controller.
   */
  public Mode getMode() {
    return this.state.getMode();
  }

  /**
   * Sets the active mode of this controller and fires a PropertyChangeEvent. 
   */
  public void setMode(Mode mode) {
    Mode oldMode = this.state.getMode();
    if (mode != oldMode) {
      this.state.setMode(mode);
      this.propertyChangeSupport.firePropertyChange(Property.MODE.name(), oldMode, mode);
    }
  }

  /**
   * Returns true if the interactions in the current mode may modify 
   * the state of a home. 
   */
  public boolean isModificationState() {
    return this.state.isModificationState();
  }

  /**
   * Returns true if the interactions in the current mode may modify 
   * the base plan of a home. 
   */
  public boolean isBasePlanModificationState() {
    return this.state.isBasePlanModificationState();
  }

  /**
   * Deletes the selection in home.
   */
  @Override
  public void deleteSelection() {
    this.state.deleteSelection();
  }

  /**
   * Escapes of current action.
   */
  public void escape() {
    this.state.escape();
  }

  /**
   * Moves the selection of (dx,dy) in home.
   */
  public void moveSelection(float dx, float dy) {
    this.state.moveSelection(dx, dy);
  }
  
  /**
   * Toggles temporary magnetism feature of user preferences. 
   * @param magnetismToggled if true then magnetism feature is toggled.
   */
  public void toggleMagnetism(boolean magnetismToggled) {
    this.state.toggleMagnetism(magnetismToggled);
  }

  /**
   * Activates or deactivates alignment feature. 
   * @param alignmentActivated if true then alignment is active.
   * @since 4.0
   */
  public void setAlignmentActivated(boolean alignmentActivated) {
    this.state.setAlignmentActivated(alignmentActivated);
  }

  /**
   * Activates or deactivates duplication feature. 
   * @param duplicationActivated if true then duplication is active.
   */
  public void setDuplicationActivated(boolean duplicationActivated) {
    this.state.setDuplicationActivated(duplicationActivated);
  }

  /**
   * Activates or deactivates edition.
   * @param editionActivated if true then edition is active 
   */
  public void setEditionActivated(boolean editionActivated) {    
    this.state.setEditionActivated(editionActivated);
  }  

  /**
   * Updates an editable property with the entered value.
   */
  public void updateEditableProperty(EditableProperty editableProperty, Object value) {
    this.state.updateEditableProperty(editableProperty, value);
  }

  /**
   * Processes a mouse button pressed event.
   */
  public void pressMouse(float x, float y, int clickCount, 
                         boolean shiftDown, boolean duplicationActivated) {
    pressMouse(x, y, clickCount, shiftDown, shiftDown, duplicationActivated, shiftDown);
  }

  /**
   * Processes a mouse button pressed event.
   * @since 4.0
   */
  public void pressMouse(float x, float y, int clickCount, boolean shiftDown, 
                         boolean alignmentActivated, boolean duplicationActivated, boolean magnetismToggled) {
    // Store the last coordinates of a mouse press
    this.xLastMousePress = x;
    this.yLastMousePress = y;
    this.xLastMouseMove = x;
    this.yLastMouseMove = y;
    this.shiftDownLastMousePress = shiftDown;
    this.alignmentActivatedLastMousePress = alignmentActivated;
    this.duplicationActivatedLastMousePress = duplicationActivated; 
    this.magnetismToggledLastMousePress = magnetismToggled;
    this.state.pressMouse(x, y, clickCount, shiftDown, duplicationActivated);
  }

  /**
   * Processes a mouse button released event.
   */
  public void releaseMouse(float x, float y) {
    this.state.releaseMouse(x, y);
  }

  /**
   * Processes a mouse button moved event.
   */
  public void moveMouse(float x, float y) {
    // Store the last coordinates of a mouse move
    this.xLastMouseMove = x;
    this.yLastMouseMove = y;
    this.state.moveMouse(x, y);
  }

  /**
   * Processes a zoom event.
   */
  public void zoom(float factor) {
    this.state.zoom(factor);
  }

  /**
   * Returns the selection state.
   */
  protected ControllerState getSelectionState() {
    return this.selectionState;
  }

  /**
   * Returns the selection move state.
   */
  protected ControllerState getSelectionMoveState() {
    return this.selectionMoveState;
  }

  /**
   * Returns the rectangle selection state.
   */
  protected ControllerState getRectangleSelectionState() {
    return this.rectangleSelectionState;
  }

  /**
   * Returns the panning state.
   */
  protected ControllerState getPanningState() {
    return this.panningState;
  }

  /**
   * Returns the drag and drop state.
   */
  protected ControllerState getDragAndDropState() {
    return this.dragAndDropState;
  }

  /**
   * Returns the wall creation state.
   */
  protected ControllerState getWallCreationState() {
    return this.wallCreationState;
  }

  /**
   * Returns the wall drawing state.
   */
  protected ControllerState getWallDrawingState() {
    return this.wallDrawingState;
  }
  
  /**
   * Returns the wall resize state.
   */
  protected ControllerState getWallResizeState() {
    return this.wallResizeState;
  }
  
  /**
   * Returns the piece rotation state.
   */
  protected ControllerState getPieceOfFurnitureRotationState() {
    return this.pieceOfFurnitureRotationState;
  }

  /**
   * Returns the piece elevation state.
   */
  protected ControllerState getPieceOfFurnitureElevationState() {
    return this.pieceOfFurnitureElevationState;
  }

  /**
   * Returns the piece height state.
   */
  protected ControllerState getPieceOfFurnitureHeightState() {
    return this.pieceOfFurnitureHeightState;
  }

  /**
   * Returns the piece resize state.
   */
  protected ControllerState getPieceOfFurnitureResizeState() {
    return this.pieceOfFurnitureResizeState;
  }

  /**
   * Returns the light power modification state.
   */
  protected ControllerState getLightPowerModificationState() {
    return this.lightPowerModificationState;
  }

  /**
   * Returns the piece name offset state.
   */
  protected ControllerState getPieceOfFurnitureNameOffsetState() {
    return this.pieceOfFurnitureNameOffsetState;
  }
  
  /**
   * Returns the piece name rotation state.
   * @since 3.6
   */
  protected ControllerState getPieceOfFurnitureNameRotationState() {
    return this.pieceOfFurnitureNameRotationState;
  }
  
  /**
   * Returns the camera yaw rotation state.
   */
  protected ControllerState getCameraYawRotationState() {
    return this.cameraYawRotationState;
  }

  /**
   * Returns the camera pitch rotation state.
   */
  protected ControllerState getCameraPitchRotationState() {
    return this.cameraPitchRotationState;
  }

  /**
   * Returns the camera elevation state.
   */
  protected ControllerState getCameraElevationState() {
    return this.cameraElevationState;
  }

  /**
   * Returns the dimension line creation state.
   */
  protected ControllerState getDimensionLineCreationState() {
    return this.dimensionLineCreationState;
  }

  /**
   * Returns the dimension line drawing state.
   */
  protected ControllerState getDimensionLineDrawingState() {
    return this.dimensionLineDrawingState;
  }

  /**
   * Returns the dimension line resize state.
   */
  protected ControllerState getDimensionLineResizeState() {
    return this.dimensionLineResizeState;
  }

  /**
   * Returns the dimension line offset state.
   */
  protected ControllerState getDimensionLineOffsetState() {
    return this.dimensionLineOffsetState;
  }
  
  /**
   * Returns the room creation state.
   */
  protected ControllerState getRoomCreationState() {
    return this.roomCreationState;
  }

  /**
   * Returns the room drawing state.
   */
  protected ControllerState getRoomDrawingState() {
    return this.roomDrawingState;
  }
  
  /**
   * Returns the room resize state.
   */
  protected ControllerState getRoomResizeState() {
    return this.roomResizeState;
  }
  
  /**
   * Returns the room area offset state.
   */
  protected ControllerState getRoomAreaOffsetState() {
    return this.roomAreaOffsetState;
  }
  
  /**
   * Returns the room area rotation state.
   * @since 3.6
   */
  protected ControllerState getRoomAreaRotationState() {
    return this.roomAreaRotationState;
  }
  
  /**
   * Returns the room name offset state.
   */
  protected ControllerState getRoomNameOffsetState() {
    return this.roomNameOffsetState;
  }
  
  /**
   * Returns the room name rotation state.
   * @since 3.6
   */
  protected ControllerState getRoomNameRotationState() {
    return this.roomNameRotationState;
  }
  
  /**
   * Returns the polyline creation state.
   * @since 5.0
   */
  protected ControllerState getPolylineCreationState() {
    return this.polylineCreationState;
  }

  /**
   * Returns the polyline drawing state.
   * @since 5.0
   */
  protected ControllerState getPolylineDrawingState() {
    return this.polylineDrawingState;
  }
  
  /**
   * Returns the polyline resize state.
   * @since 5.0
   */
  protected ControllerState getPolylineResizeState() {
    return this.polylineResizeState;
  }
  
  /**
   * Returns the label creation state.
   */
  protected ControllerState getLabelCreationState() {
    return this.labelCreationState;
  }

  /**
   * Returns the label rotation state.
   * @since 3.6
   */
  protected ControllerState getLabelRotationState() {
    return this.labelRotationState;
  }

  /**
   * Returns the label elevation state.
   * @since 5.0
   */
  protected ControllerState getLabelElevationState() {
    return this.labelElevationState;
  }

  /**
   * Returns the compass rotation state.
   */
  protected ControllerState getCompassRotationState() {
    return this.compassRotationState;
  }

  /**
   * Returns the compass resize state.
   */
  protected ControllerState getCompassResizeState() {
    return this.compassResizeState;
  }

  /**
   * Returns the abscissa of mouse position at last mouse press.
   */
  protected float getXLastMousePress() {
    return this.xLastMousePress;
  }

  /**
   * Returns the ordinate of mouse position at last mouse press.
   */
  protected float getYLastMousePress() {
    return this.yLastMousePress;
  }
  
  /**
   * Returns true if shift key was down at last mouse press.
   */
  protected boolean wasShiftDownLastMousePress() {
    return this.shiftDownLastMousePress;
  }

  /**
   * Returns true if magnetism was toggled at last mouse press.
   * @since 4.0
   */
  protected boolean wasMagnetismToggledLastMousePress() {
    return this.magnetismToggledLastMousePress;
  }

  /**
   * Returns true if alignment was activated at last mouse press.
   * @since 4.0
   */
  protected boolean wasAlignmentActivatedLastMousePress() {
    return this.alignmentActivatedLastMousePress;
  }

  /**
   * Returns true if duplication was activated at last mouse press.
   */
  protected boolean wasDuplicationActivatedLastMousePress() {
    return this.duplicationActivatedLastMousePress;
  }

  /**
   * Returns the abscissa of mouse position at last mouse move.
   */
  protected float getXLastMouseMove() {
    return this.xLastMouseMove;
  }

  /**
   * Returns the ordinate of mouse position at last mouse move.
   */
  protected float getYLastMouseMove() {
    return this.yLastMouseMove;
  }
  
  /**
   * Controls the modification of selected walls.
   */
  public void modifySelectedWalls() {
    if (!Home.getWallsSubList(this.home.getSelectedItems()).isEmpty()) {
      new WallController(this.home, this.preferences, this.viewFactory,
          this.contentManager, this.undoSupport).displayView(getView());
    }
  }
  
  /**
   * Locks home base plan.
   */
  public void lockBasePlan() {
    if (!this.home.isBasePlanLocked()) {
      final boolean allLevelsSelection = this.home.isAllLevelsSelection();
      List selection = this.home.getSelectedItems();
      final Selectable [] oldSelectedItems = 
          selection.toArray(new Selectable [selection.size()]);
      
      List newSelection = getItemsNotPartOfBasePlan(selection);
      final Selectable [] newSelectedItems = 
        newSelection.toArray(new Selectable [newSelection.size()]);
      
      this.home.setBasePlanLocked(true);
      selectItems(newSelection, allLevelsSelection);
      UndoableEdit undoableEdit = new AbstractUndoableEdit() {      
        @Override
        public void undo() throws CannotUndoException {
          super.undo();
          home.setBasePlanLocked(false);
          selectAndShowItems(Arrays.asList(oldSelectedItems), allLevelsSelection);
        }
        
        @Override
        public void redo() throws CannotRedoException {
          super.redo();
          home.setBasePlanLocked(true);
          selectAndShowItems(Arrays.asList(newSelectedItems), allLevelsSelection);
        }      
    
        @Override
        public String getPresentationName() {
          return preferences.getLocalizedString(
              PlanController.class, "undoLockBasePlan");
        }      
      };
      this.undoSupport.postEdit(undoableEdit);
    }
  }

  /**
   * Returns true it the given item belongs
   * to the base plan.
   */
  protected boolean isItemPartOfBasePlan(Selectable item) {
    if (item instanceof HomePieceOfFurniture) {
      return isPieceOfFurniturePartOfBasePlan((HomePieceOfFurniture)item);
    } else {
      return !(item instanceof ObserverCamera);
    }
  }

  /**
   * Returns the items among the given list that are not part of the base plan.
   */
  private List getItemsNotPartOfBasePlan(List items) {
    List itemsNotPartOfBasePlan = new ArrayList();
    for (Selectable item : items) {
      if (!isItemPartOfBasePlan(item)) {
        itemsNotPartOfBasePlan.add(item);
      }
    }
    return itemsNotPartOfBasePlan;
  }

  /**
   * Unlocks home base plan.
   */
  public void unlockBasePlan() {
    if (this.home.isBasePlanLocked()) {
      final boolean allLevelsSelection = this.home.isAllLevelsSelection();
      List selection = this.home.getSelectedItems();
      final Selectable [] selectedItems = 
          selection.toArray(new Selectable [selection.size()]);
      
      this.home.setBasePlanLocked(false);
      this.home.setAllLevelsSelection(false);
      UndoableEdit undoableEdit = new AbstractUndoableEdit() {      
        @Override
        public void undo() throws CannotUndoException {
          super.undo();
          home.setBasePlanLocked(true);
          selectAndShowItems(Arrays.asList(selectedItems), allLevelsSelection);
        }
        
        @Override
        public void redo() throws CannotRedoException {
          super.redo();
          home.setBasePlanLocked(false);
          selectAndShowItems(Arrays.asList(selectedItems), false);
        }      
    
        @Override
        public String getPresentationName() {
          return preferences.getLocalizedString(
              PlanController.class, "undoUnlockBasePlan");
        }      
      };
      this.undoSupport.postEdit(undoableEdit);
    }
  }

  /**
   * Returns true if the given item may be moved
   * in the plan. Default implementation returns true. 
   */
  protected boolean isItemMovable(Selectable item) {
    if (item instanceof HomePieceOfFurniture) {
      return isPieceOfFurnitureMovable((HomePieceOfFurniture)item);
    } else {
      return true;
    }
  }
  
  /**
   * Returns true if the given item may be resized.
   * Default implementation returns false if the given item
   * is a non resizable piece of furniture.
   */
  protected boolean isItemResizable(Selectable item) {
    if (item instanceof HomePieceOfFurniture) {
      return ((HomePieceOfFurniture)item).isResizable();
    } else {
      return true;
    }
  }
  
  /**
   * Returns true if the given item may be deleted.
   * Default implementation returns true except if the given item
   * is a camera or a compass or if the given item isn't a 
   * {@linkplain #isPieceOfFurnitureDeletable(HomePieceOfFurniture) deletable piece of furniture}. 
   */
  protected boolean isItemDeletable(Selectable item) {
    if (item instanceof HomePieceOfFurniture) {
      return isPieceOfFurnitureDeletable((HomePieceOfFurniture)item);
    } else {
      return !(item instanceof Compass || item instanceof Camera);
    }
  }
  
  /**
   * Controls the direction reverse of selected walls.
   */
  public void reverseSelectedWallsDirection() {
    List selectedWalls = Home.getWallsSubList(this.home.getSelectedItems());
    if (!selectedWalls.isEmpty()) {
      Wall [] reversedWalls = selectedWalls.toArray(new Wall [selectedWalls.size()]);
      doReverseWallsDirection(reversedWalls);
      postReverseSelectedWallsDirection(reversedWalls, this.home.getSelectedItems());
    }
  }
  
  /**
   * Posts an undoable reverse wall operation, about walls.
   */
  private void postReverseSelectedWallsDirection(final Wall [] walls, 
                                                 List oldSelection) {
    final boolean allLevelsSelection = home.isAllLevelsSelection();
    final Selectable [] oldSelectedItems = 
        oldSelection.toArray(new Selectable [oldSelection.size()]);
    UndoableEdit undoableEdit = new AbstractUndoableEdit() {      
      @Override
      public void undo() throws CannotUndoException {
        super.undo();
        doReverseWallsDirection(walls);
        selectAndShowItems(Arrays.asList(oldSelectedItems), allLevelsSelection);
      }
      
      @Override
      public void redo() throws CannotRedoException {
        super.redo();
        doReverseWallsDirection(walls);
        selectAndShowItems(Arrays.asList(oldSelectedItems), allLevelsSelection);
      }      

      @Override
      public String getPresentationName() {
        return preferences.getLocalizedString(
            PlanController.class, "undoReverseWallsDirectionName");
      }      
    };
    this.undoSupport.postEdit(undoableEdit);
  }

  /**
   * Reverses the walls direction.
   */
  private void doReverseWallsDirection(Wall [] walls) {
    for (Wall wall : walls) {
      float xStart = wall.getXStart();
      float yStart = wall.getYStart();
      float xEnd = wall.getXEnd();
      float yEnd = wall.getYEnd();
      wall.setXStart(xEnd);
      wall.setYStart(yEnd);
      wall.setXEnd(xStart);
      wall.setYEnd(yStart);
      if (wall.getArcExtent() != null) {
        wall.setArcExtent(-wall.getArcExtent());
      }

      Wall wallAtStart = wall.getWallAtStart();            
      boolean joinedAtEndOfWallAtStart =
        wallAtStart != null
        && wallAtStart.getWallAtEnd() == wall;
      boolean joinedAtStartOfWallAtStart =
        wallAtStart != null
        && wallAtStart.getWallAtStart() == wall;
      Wall wallAtEnd = wall.getWallAtEnd();      
      boolean joinedAtEndOfWallAtEnd =
        wallAtEnd != null
        && wallAtEnd.getWallAtEnd() == wall;
      boolean joinedAtStartOfWallAtEnd =
        wallAtEnd != null
        && wallAtEnd.getWallAtStart() == wall;
      
      wall.setWallAtStart(wallAtEnd);
      wall.setWallAtEnd(wallAtStart);
      
      if (joinedAtEndOfWallAtStart) {
        wallAtStart.setWallAtEnd(wall);
      } else if (joinedAtStartOfWallAtStart) {
        wallAtStart.setWallAtStart(wall);
      }
      
      if (joinedAtEndOfWallAtEnd) {
        wallAtEnd.setWallAtEnd(wall);
      } else if (joinedAtStartOfWallAtEnd) {
        wallAtEnd.setWallAtStart(wall);
      }
      
      Integer rightSideColor = wall.getRightSideColor();
      HomeTexture rightSideTexture = wall.getRightSideTexture();
      float leftSideShininess = wall.getLeftSideShininess();
      Baseboard leftSideBaseboard = wall.getLeftSideBaseboard();
      Integer leftSideColor = wall.getLeftSideColor();
      HomeTexture leftSideTexture = wall.getLeftSideTexture();
      float rightSideShininess = wall.getRightSideShininess();
      Baseboard rightSideBaseboard = wall.getRightSideBaseboard();
      wall.setLeftSideColor(rightSideColor);
      wall.setLeftSideTexture(rightSideTexture);
      wall.setLeftSideShininess(rightSideShininess);
      wall.setLeftSideBaseboard(rightSideBaseboard);
      wall.setRightSideColor(leftSideColor);
      wall.setRightSideTexture(leftSideTexture);
      wall.setRightSideShininess(leftSideShininess);
      wall.setRightSideBaseboard(leftSideBaseboard);
      
      Float heightAtEnd = wall.getHeightAtEnd();
      if (heightAtEnd != null) {
        Float height = wall.getHeight();
        wall.setHeight(heightAtEnd);
        wall.setHeightAtEnd(height);
      }
    }
  }
  
  /**
   * Controls the split of the selected wall in two joined walls of equal length.
   */
  public void splitSelectedWall() {
    List selectedItems = this.home.getSelectedItems();
    List selectedWalls = Home.getWallsSubList(selectedItems);
    if (selectedWalls.size() == 1) {
      boolean allLevelsSelection = this.home.isAllLevelsSelection();
      boolean basePlanLocked = this.home.isBasePlanLocked();
      Wall splitWall = selectedWalls.get(0);
      JoinedWall splitJoinedWall = new JoinedWall(splitWall);
      float xStart = splitWall.getXStart();
      float yStart = splitWall.getYStart();
      float xEnd = splitWall.getXEnd();
      float yEnd = splitWall.getYEnd();
      float xMiddle = (xStart + xEnd) / 2;
      float yMiddle = (yStart + yEnd) / 2;

      Wall wallAtStart = splitWall.getWallAtStart();            
      boolean joinedAtEndOfWallAtStart =
        wallAtStart != null
        && wallAtStart.getWallAtEnd() == splitWall;
      boolean joinedAtStartOfWallAtStart =
        wallAtStart != null
        && wallAtStart.getWallAtStart() == splitWall;
      Wall wallAtEnd = splitWall.getWallAtEnd();      
      boolean joinedAtEndOfWallAtEnd =
        wallAtEnd != null
        && wallAtEnd.getWallAtEnd() == splitWall;
      boolean joinedAtStartOfWallAtEnd =
        wallAtEnd != null
        && wallAtEnd.getWallAtStart() == splitWall;

      // Clone new walls to copy their characteristics 
      Wall firstWall = splitWall.clone();
      this.home.addWall(firstWall);
      firstWall.setLevel(splitWall.getLevel());
      Wall secondWall = splitWall.clone();
      this.home.addWall(secondWall);
      secondWall.setLevel(splitWall.getLevel());
      
      // Change split walls end and start point
      firstWall.setXEnd(xMiddle);
      firstWall.setYEnd(yMiddle);
      secondWall.setXStart(xMiddle);
      secondWall.setYStart(yMiddle);
      if (splitWall.getHeightAtEnd() != null) {
        Float heightAtMiddle = (splitWall.getHeight() + splitWall.getHeightAtEnd()) / 2;
        firstWall.setHeightAtEnd(heightAtMiddle);
        secondWall.setHeight(heightAtMiddle);
      } 
            
      firstWall.setWallAtEnd(secondWall);
      secondWall.setWallAtStart(firstWall);
      
      firstWall.setWallAtStart(wallAtStart);
      if (joinedAtEndOfWallAtStart) {
        wallAtStart.setWallAtEnd(firstWall);
      } else if (joinedAtStartOfWallAtStart) {
        wallAtStart.setWallAtStart(firstWall);
      }
      
      secondWall.setWallAtEnd(wallAtEnd);
      if (joinedAtEndOfWallAtEnd) {
        wallAtEnd.setWallAtEnd(secondWall);
      } else if (joinedAtStartOfWallAtEnd) {
        wallAtEnd.setWallAtStart(secondWall);
      }
      
      // Delete split wall
      this.home.deleteWall(splitWall);
      selectAndShowItems(Arrays.asList(new Wall [] {firstWall}), false);
      
      postSplitSelectedWall(splitJoinedWall, 
          new JoinedWall(firstWall), new JoinedWall(secondWall), selectedItems, basePlanLocked, allLevelsSelection);
    }
  }
  
  /**
   * Posts an undoable split wall operation.
   */
  private void postSplitSelectedWall(final JoinedWall splitJoinedWall, 
                                     final JoinedWall firstJoinedWall, 
                                     final JoinedWall secondJoinedWall,
                                     List oldSelection,
                                     final boolean oldBasePlanLocked, 
                                     final boolean oldAllLevelsSelection) {
    final Selectable [] oldSelectedItems = 
        oldSelection.toArray(new Selectable [oldSelection.size()]);
    final boolean newBasePlanLocked = this.home.isBasePlanLocked();
    UndoableEdit undoableEdit = new AbstractUndoableEdit() {      
      @Override
      public void undo() throws CannotUndoException {
        super.undo();
        doDeleteWalls(new JoinedWall [] {firstJoinedWall, secondJoinedWall}, oldBasePlanLocked);
        doAddWalls(new JoinedWall [] {splitJoinedWall}, oldBasePlanLocked);
        selectAndShowItems(Arrays.asList(oldSelectedItems), oldAllLevelsSelection);
      }
      
      @Override
      public void redo() throws CannotRedoException {
        super.redo();
        doDeleteWalls(new JoinedWall [] {splitJoinedWall}, newBasePlanLocked);
        doAddWalls(new JoinedWall [] {firstJoinedWall, secondJoinedWall}, newBasePlanLocked);
        selectAndShowItems(Arrays.asList(new Wall [] {firstJoinedWall.getWall()}), false);
      }      

      @Override
      public String getPresentationName() {
        return preferences.getLocalizedString(
            PlanController.class, "undoSplitWallName");
      }      
    };
    this.undoSupport.postEdit(undoableEdit);
  }

  /**
   * Controls the modification of the selected rooms.
   */
  public void modifySelectedRooms() {
    if (!Home.getRoomsSubList(this.home.getSelectedItems()).isEmpty()) {
      new RoomController(this.home, this.preferences, this.viewFactory,
          this.contentManager, this.undoSupport).displayView(getView());
    }
  }
  
  /**
   * Returns a new label. The new label isn't added to home.
   */
  private void createLabel(float x, float y) {
    new LabelController(this.home, x, y, this.preferences, this.viewFactory,
        this.undoSupport).displayView(getView());
  }
  
  /**
   * Controls the modification of the selected labels.
   */
  public void modifySelectedLabels() {
    if (!Home.getLabelsSubList(this.home.getSelectedItems()).isEmpty()) {
      new LabelController(this.home, this.preferences, this.viewFactory,
          this.undoSupport).displayView(getView());
    }
  }
  
  /**
   * Controls the modification of the selected polylines.
   * @since 5.0
   */
  public void modifySelectedPolylines() {
    if (!Home.getPolylinesSubList(this.home.getSelectedItems()).isEmpty()) {
      new PolylineController(this.home, this.preferences, this.viewFactory,
          this.contentManager, this.undoSupport).displayView(getView());
    }
  }

  /**
   * Controls the modification of the compass.
   */
  public void modifyCompass() {
    new CompassController(this.home, this.preferences, this.viewFactory,
        this.undoSupport).displayView(getView());
  }
  
  /**
   * Controls the modification of the observer camera.
   */
  public void modifyObserverCamera() {
    new ObserverCameraController(this.home, this.preferences, this.viewFactory).displayView(getView());
  }
  
  /**
   * Toggles bold style of texts in selected items.
   */
  public void toggleBoldStyle() {
    // Find if selected items are all bold or not
    Boolean selectionBoldStyle = null;
    for (Selectable item : this.home.getSelectedItems()) {
      Boolean bold;
      if (item instanceof Label) {
        bold = getItemTextStyle(item, ((Label)item).getStyle()).isBold();
      } else if (item instanceof HomePieceOfFurniture
          && ((HomePieceOfFurniture)item).isVisible()) {
        bold = getItemTextStyle(item, ((HomePieceOfFurniture)item).getNameStyle()).isBold();
      } else if (item instanceof Room) {
        Room room = (Room)item;
        bold = getItemTextStyle(room, room.getNameStyle()).isBold();
        if (bold != getItemTextStyle(room, room.getAreaStyle()).isBold()) {
          bold = null;
        }
      } else if (item instanceof DimensionLine) {
        bold = getItemTextStyle(item, ((DimensionLine)item).getLengthStyle()).isBold();
      } else {
        continue;
      }
      if (selectionBoldStyle == null) {
        selectionBoldStyle = bold;
      } else if (bold == null || !selectionBoldStyle.equals(bold)) {
        selectionBoldStyle = null;
        break;
      }
    }
    
    // Apply new bold style to all selected items
    if (selectionBoldStyle == null) {
      selectionBoldStyle = Boolean.TRUE;
    } else {
      selectionBoldStyle = !selectionBoldStyle;
    }

    List itemsWithText = new ArrayList(); 
    List oldTextStyles = new ArrayList(); 
    List textStyles = new ArrayList(); 
    for (Selectable item : this.home.getSelectedItems()) {
      if (item instanceof Label) {
        Label label = (Label)item;
        itemsWithText.add(label);
        TextStyle oldTextStyle = getItemTextStyle(label, label.getStyle());
        oldTextStyles.add(oldTextStyle);
        textStyles.add(oldTextStyle.deriveBoldStyle(selectionBoldStyle));
      } else if (item instanceof HomePieceOfFurniture) {
        HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
        if (piece.isVisible()) {
          itemsWithText.add(piece);
          TextStyle oldNameStyle = getItemTextStyle(piece, piece.getNameStyle());
          oldTextStyles.add(oldNameStyle);
          textStyles.add(oldNameStyle.deriveBoldStyle(selectionBoldStyle));
        }
      } else if (item instanceof Room) {
        final Room room = (Room)item;
        itemsWithText.add(room);
        TextStyle oldNameStyle = getItemTextStyle(room, room.getNameStyle());
        oldTextStyles.add(oldNameStyle);
        textStyles.add(oldNameStyle.deriveBoldStyle(selectionBoldStyle));
        TextStyle oldAreaStyle = getItemTextStyle(room, room.getAreaStyle());
        oldTextStyles.add(oldAreaStyle);
        textStyles.add(oldAreaStyle.deriveBoldStyle(selectionBoldStyle));
      } else if (item instanceof DimensionLine) {
        DimensionLine dimensionLine = (DimensionLine)item;
        itemsWithText.add(dimensionLine);
        TextStyle oldLengthStyle = getItemTextStyle(dimensionLine, dimensionLine.getLengthStyle());
        oldTextStyles.add(oldLengthStyle);
        textStyles.add(oldLengthStyle.deriveBoldStyle(selectionBoldStyle));
      } 
    }
    modifyTextStyle(itemsWithText.toArray(new Selectable [itemsWithText.size()]),
        oldTextStyles.toArray(new TextStyle [oldTextStyles.size()]),
        textStyles.toArray(new TextStyle [textStyles.size()]));
  }
  
  /**
   * Returns textStyle if not null or the default text style.
   */
  private TextStyle getItemTextStyle(Selectable item, TextStyle textStyle) {
    if (textStyle == null) {
      textStyle = this.preferences.getDefaultTextStyle(item.getClass());              
    }          
    return textStyle;
  }
  
  /**
   * Toggles italic style of texts in selected items.
   */
  public void toggleItalicStyle() {
    // Find if selected items are all italic or not
    Boolean selectionItalicStyle = null;
    for (Selectable item : this.home.getSelectedItems()) {
      Boolean italic;
      if (item instanceof Label) {
        italic = getItemTextStyle(item, ((Label)item).getStyle()).isItalic();
      } else if (item instanceof HomePieceOfFurniture
          && ((HomePieceOfFurniture)item).isVisible()) {
        italic = getItemTextStyle(item, ((HomePieceOfFurniture)item).getNameStyle()).isItalic();
      } else if (item instanceof Room) {
        Room room = (Room)item;
        italic = getItemTextStyle(room, room.getNameStyle()).isItalic();
        if (italic != getItemTextStyle(room, room.getAreaStyle()).isItalic()) {
          italic = null;
        }
      } else if (item instanceof DimensionLine) {
        italic = getItemTextStyle(item, ((DimensionLine)item).getLengthStyle()).isItalic();
      } else {
        continue;
      }
      if (selectionItalicStyle == null) {
        selectionItalicStyle = italic;
      } else if (italic == null || !selectionItalicStyle.equals(italic)) {
        selectionItalicStyle = null;
        break;
      }
    }
    
    // Apply new italic style to all selected items
    if (selectionItalicStyle == null) {
      selectionItalicStyle = Boolean.TRUE;
    } else {
      selectionItalicStyle = !selectionItalicStyle; 
    }
    
    List itemsWithText = new ArrayList(); 
    List oldTextStyles = new ArrayList(); 
    List textStyles = new ArrayList(); 
    for (Selectable item : this.home.getSelectedItems()) {
      if (item instanceof Label) {
        Label label = (Label)item;
        itemsWithText.add(label);
        TextStyle oldTextStyle = getItemTextStyle(label, label.getStyle());
        oldTextStyles.add(oldTextStyle);
        textStyles.add(oldTextStyle.deriveItalicStyle(selectionItalicStyle));
      } else if (item instanceof HomePieceOfFurniture) {
        HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
        if (piece.isVisible()) {
          itemsWithText.add(piece);
          TextStyle oldNameStyle = getItemTextStyle(piece, piece.getNameStyle());
          oldTextStyles.add(oldNameStyle);
          textStyles.add(oldNameStyle.deriveItalicStyle(selectionItalicStyle));
        }
      } else if (item instanceof Room) {
        final Room room = (Room)item;
        itemsWithText.add(room);
        TextStyle oldNameStyle = getItemTextStyle(room, room.getNameStyle());
        oldTextStyles.add(oldNameStyle);
        textStyles.add(oldNameStyle.deriveItalicStyle(selectionItalicStyle));
        TextStyle oldAreaStyle = getItemTextStyle(room, room.getAreaStyle());
        oldTextStyles.add(oldAreaStyle);
        textStyles.add(oldAreaStyle.deriveItalicStyle(selectionItalicStyle));
      } else if (item instanceof DimensionLine) {
        DimensionLine dimensionLine = (DimensionLine)item;
        itemsWithText.add(dimensionLine);
        TextStyle oldLengthStyle = getItemTextStyle(dimensionLine, dimensionLine.getLengthStyle());
        oldTextStyles.add(oldLengthStyle);
        textStyles.add(oldLengthStyle.deriveItalicStyle(selectionItalicStyle));
      } 
    }
    modifyTextStyle(itemsWithText.toArray(new Selectable [itemsWithText.size()]),
        oldTextStyles.toArray(new TextStyle [oldTextStyles.size()]),
        textStyles.toArray(new TextStyle [textStyles.size()]));
  }
  
  /**
   * Increase the size of texts in selected items.
   */
  public void increaseTextSize() {
    applyFactorToTextSize(1.1f);
  }
  
  /**
   * Decrease the size of texts in selected items.
   */
  public void decreaseTextSize() {
    applyFactorToTextSize(1 / 1.1f);
  }

  /**
   * Applies a factor to the font size of the texts of the selected items in home.
   */
  private void applyFactorToTextSize(float factor) {
    List itemsWithText = new ArrayList(); 
    List oldTextStyles = new ArrayList(); 
    List textStyles = new ArrayList(); 
    for (Selectable item : this.home.getSelectedItems()) {
      if (item instanceof Label) {
        Label label = (Label)item;
        itemsWithText.add(label);
        TextStyle oldLabelStyle = getItemTextStyle(item, label.getStyle());
        oldTextStyles.add(oldLabelStyle);
        textStyles.add(oldLabelStyle.deriveStyle(Math.round(oldLabelStyle.getFontSize() * factor)));
      } else if (item instanceof HomePieceOfFurniture) {
        HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
        if (piece.isVisible()) {
          itemsWithText.add(piece);
          TextStyle oldNameStyle = getItemTextStyle(piece, piece.getNameStyle());
          oldTextStyles.add(oldNameStyle);
          textStyles.add(oldNameStyle.deriveStyle(Math.round(oldNameStyle.getFontSize() * factor)));
        }
      } else if (item instanceof Room) {
        final Room room = (Room)item;
        itemsWithText.add(room);
        TextStyle oldNameStyle = getItemTextStyle(room, room.getNameStyle());
        oldTextStyles.add(oldNameStyle);
        textStyles.add(oldNameStyle.deriveStyle(Math.round(oldNameStyle.getFontSize() * factor)));
        TextStyle oldAreaStyle = getItemTextStyle(room, room.getAreaStyle());
        oldTextStyles.add(oldAreaStyle);
        textStyles.add(oldAreaStyle.deriveStyle(Math.round(oldAreaStyle.getFontSize() * factor)));
      } else if (item instanceof DimensionLine) {
        DimensionLine dimensionLine = (DimensionLine)item;
        itemsWithText.add(dimensionLine);
        TextStyle oldLengthStyle = getItemTextStyle(dimensionLine, dimensionLine.getLengthStyle());
        oldTextStyles.add(oldLengthStyle);
        textStyles.add(oldLengthStyle.deriveStyle(Math.round(oldLengthStyle.getFontSize() * factor)));
      } 
    }
    modifyTextStyle(itemsWithText.toArray(new Selectable [itemsWithText.size()]), 
        oldTextStyles.toArray(new TextStyle [oldTextStyles.size()]),
        textStyles.toArray(new TextStyle [textStyles.size()]));
  }
  
  /**
   * Changes the style of items and posts an undoable change style operation.
   */
  private void modifyTextStyle(final Selectable [] items, 
                               final TextStyle [] oldStyles,
                               final TextStyle [] styles) {
    final boolean allLevelsSelection = home.isAllLevelsSelection();
    List oldSelection = this.home.getSelectedItems();
    final Selectable [] oldSelectedItems = 
        oldSelection.toArray(new Selectable [oldSelection.size()]);
    
    doModifyTextStyle(items, styles);
    UndoableEdit undoableEdit = new AbstractUndoableEdit() {      
      @Override
      public void undo() throws CannotUndoException {
        super.undo();
        doModifyTextStyle(items, oldStyles);
        selectAndShowItems(Arrays.asList(oldSelectedItems), allLevelsSelection);
      }
      
      @Override
      public void redo() throws CannotRedoException {
        super.redo();
        doModifyTextStyle(items, styles);
        selectAndShowItems(Arrays.asList(oldSelectedItems), allLevelsSelection);
      }      

      @Override
      public String getPresentationName() {
        return preferences.getLocalizedString(PlanController.class, "undoModifyTextStyleName");
      }      
    };
    this.undoSupport.postEdit(undoableEdit);
  }
  
  /**
   * Changes the style of items.
   */
  private void doModifyTextStyle(Selectable [] items, TextStyle [] styles) {
    int styleIndex = 0;
    for (Selectable item : items) {
      if (item instanceof Label) {
        ((Label)item).setStyle(styles [styleIndex++]);
      } else if (item instanceof HomePieceOfFurniture) {
        HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
        if (piece.isVisible()) {
          piece.setNameStyle(styles [styleIndex++]);
        }
      } else if (item instanceof Room) {
        final Room room = (Room)item;
        room.setNameStyle(styles [styleIndex++]);
        room.setAreaStyle(styles [styleIndex++]);
      } else if (item instanceof DimensionLine) {
        ((DimensionLine)item).setLengthStyle(styles [styleIndex++]);
      } 
    }
  }

  /**
   * Returns the minimum scale of the plan view.
   */
  public float getMinimumScale() {
    return 0.01f;
  }
  
  /**
   * Returns the maximum scale of the plan view.
   */
  public float getMaximumScale() {
    return 5f;
  }
  
  /**
   * Returns the scale in plan view. 
   */
  public float getScale() {
    return getView().getScale();
  }

  /**
   * Controls the scale in plan view and and fires a PropertyChangeEvent. 
   */
  public void setScale(float scale) {
    scale = Math.max(getMinimumScale(), Math.min(scale, getMaximumScale()));
    if (scale != getView().getScale()) {
      float oldScale = getView().getScale();
      this.furnitureSidesCache.clear();
      if (getView() != null) {
        int x = getView().convertXModelToScreen(getXLastMouseMove());
        int y = getView().convertXModelToScreen(getYLastMouseMove());
        getView().setScale(scale);
        // Update mouse location
        moveMouse(getView().convertXPixelToModel(x), getView().convertYPixelToModel(y));
      }
      this.propertyChangeSupport.firePropertyChange(Property.SCALE.name(), oldScale, scale);
      this.home.setProperty(SCALE_VISUAL_PROPERTY, String.valueOf(scale));
    }
  } 
  
  /**
   * Sets the selected level in home.
   */
  public void setSelectedLevel(Level level) {
    this.home.setSelectedLevel(level);
  }

  /**
   * Selects all visible items in the selected level of home.
   */
  @Override
  public void selectAll() {
    List all = getVisibleItemsAtSelectedLevel();
    if (this.home.isBasePlanLocked()) {
      this.home.setSelectedItems(getItemsNotPartOfBasePlan(all));
    } else {
      this.home.setSelectedItems(all);
    }
    this.home.setAllLevelsSelection(false);
  }

  /**
   * Returns the viewable and selectable home items at the selected level, except camera. 
   */
  private List getVisibleItemsAtSelectedLevel() {
    List selectableItems = new ArrayList();
    Level selectedLevel = this.home.getSelectedLevel();
    for (Selectable item : this.home.getSelectableViewableItems()) {
      if (item instanceof HomePieceOfFurniture) {
        if (isPieceOfFurnitureVisibleAtSelectedLevel((HomePieceOfFurniture)item)) {
          selectableItems.add(item);
        }
      } else if (!(item instanceof Elevatable)
          || ((Elevatable)item).isAtLevel(selectedLevel)) {
        selectableItems.add(item);
      }
    }
    return selectableItems;
  }
  
  /**
   * Selects all visible items in all levels of home.
   * @since 4.4
   */
  public void selectAllAtAllLevels() {
    List allItems = new ArrayList(this.home.getSelectableViewableItems());
    if (this.home.isBasePlanLocked()) {
      allItems = getItemsNotPartOfBasePlan(allItems);
    }
    this.home.setSelectedItems(allItems); 
    this.home.setAllLevelsSelection(true);
  }

  /**
   * Returns the visible (fully or partially) rooms at the selected level in home.
   */
  private List getDetectableRoomsAtSelectedLevel() {
    List rooms = this.home.getRooms();
    Level selectedLevel = this.home.getSelectedLevel();
    List levels = this.home.getLevels();
    if (selectedLevel == null || levels.size() <= 1) {
      return rooms; 
    } else {
      List visibleRooms = new ArrayList(rooms.size());
      int selectedLevelIndex = levels.indexOf(selectedLevel);
      boolean level0 = levels.get(0) == selectedLevel
          || levels.get(selectedLevelIndex - 1).getElevation() == selectedLevel.getElevation();
      Level otherLevel = levels.get(level0 && selectedLevelIndex < levels.size() - 1 
          ? selectedLevelIndex + 1 
          : selectedLevelIndex  - 1);
      for (Room room : rooms) {
        if (room.isAtLevel(selectedLevel)
            || otherLevel != null 
                && room.isAtLevel(otherLevel)
                && (level0 && room.isFloorVisible()
                    || !level0 && room.isCeilingVisible())) {
          visibleRooms.add(room);
        }
      }      
      return visibleRooms;
    }
  }
  
  /**
   * Returns the visible (fully or partially) walls at the selected level in home.
   */
  private Collection getDetectableWallsAtSelectedLevel() {
    Collection walls = this.home.getWalls();
    Level selectedLevel = this.home.getSelectedLevel();
    List levels = this.home.getLevels();
    if (selectedLevel == null || levels.size() <= 1) {
      return walls; 
    } else {
      Collection visibleWalls = new ArrayList(walls.size());
      int selectedLevelIndex = levels.indexOf(selectedLevel);
      boolean level0 = levels.get(0) == selectedLevel
          || levels.get(selectedLevelIndex - 1).getElevation() == selectedLevel.getElevation();
      Level otherLevel = levels.get(level0 && selectedLevelIndex < levels.size() - 1 
          ? selectedLevelIndex + 1 
          : selectedLevelIndex  - 1);
      for (Wall wall : walls) {
        if (wall.isAtLevel(selectedLevel)
            || otherLevel != null 
               && wall.isAtLevel(otherLevel)) {
          visibleWalls.add(wall);
        }
      }
      return visibleWalls;
    }
  }

  /**
   * Returns the horizontal ruler of the plan view. 
   */
  public View getHorizontalRulerView() {
    return getView().getHorizontalRuler();
  }
  
  /**
   * Returns the vertical ruler of the plan view. 
   */
  public View getVerticalRulerView() {
    return getView().getVerticalRuler();
  }
  
  private void addModelListeners() {
    this.selectionListener = new SelectionListener() {
        public void selectionChanged(SelectionEvent ev) {
          selectLevelFromSelectedItems();
          if (getView() != null) {
            getView().makeSelectionVisible();
          }
        }
      };
    this.home.addSelectionListener(this.selectionListener);
    // Ensure observer camera is visible when its size, location or angles change
    this.home.getObserverCamera().addPropertyChangeListener(new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          if (home.getSelectedItems().contains(ev.getSource())) {
            if (getView() != null) {
              getView().makeSelectionVisible();
            }
          }
        }
      });
    this.wallChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          String propertyName = ev.getPropertyName();
          if (Wall.Property.X_START.name().equals(propertyName)
              || Wall.Property.X_END.name().equals(propertyName) 
              || Wall.Property.Y_START.name().equals(propertyName) 
              || Wall.Property.Y_END.name().equals(propertyName)
              || Wall.Property.WALL_AT_START.name().equals(propertyName)
              || Wall.Property.WALL_AT_END.name().equals(propertyName)
              || Wall.Property.THICKNESS.name().equals(propertyName)
              || Wall.Property.ARC_EXTENT.name().equals(propertyName)
              || Wall.Property.LEVEL.name().equals(propertyName)
              || Wall.Property.HEIGHT.name().equals(propertyName)
              || Wall.Property.HEIGHT_AT_END.name().equals(propertyName)
              || Wall.Property.LEFT_SIDE_BASEBOARD.name().equals(propertyName)
              || Wall.Property.RIGHT_SIDE_BASEBOARD.name().equals(propertyName)) {
            resetAreaCache();
            // Unselect unreachable wall
            Wall wall = (Wall)ev.getSource();
            if (!wall.isAtLevel(home.getSelectedLevel())) {
              List selectedItems = new ArrayList(home.getSelectedItems());
              if (selectedItems.remove(wall)) {
                selectItems(selectedItems, home.isAllLevelsSelection());
              }
            }
          }
        }
      };
    for (Wall wall : this.home.getWalls()) {
      wall.addPropertyChangeListener(this.wallChangeListener);
    }
    this.home.addWallsListener(new CollectionListener () {
        public void collectionChanged(CollectionEvent ev) {
          if (ev.getType() == CollectionEvent.Type.ADD) {
            ev.getItem().addPropertyChangeListener(wallChangeListener);
          } else if (ev.getType() == CollectionEvent.Type.DELETE) {
            ev.getItem().removePropertyChangeListener(wallChangeListener);
          }
          resetAreaCache();
        }
      });
    // Add listener to update furnitureBordersCache when walls change
    final PropertyChangeListener furnitureChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          String propertyName = ev.getPropertyName();
          if (HomePieceOfFurniture.Property.X.name().equals(propertyName)
              || HomePieceOfFurniture.Property.Y.name().equals(propertyName)
              || HomePieceOfFurniture.Property.WIDTH.name().equals(propertyName)
              || HomePieceOfFurniture.Property.DEPTH.name().equals(propertyName)) {
            furnitureSidesCache.remove((HomePieceOfFurniture)ev.getSource());
          }
        }
      };
    for (HomePieceOfFurniture piece : this.home.getFurniture()) {
      piece.addPropertyChangeListener(furnitureChangeListener);
    }
    this.home.addFurnitureListener(new CollectionListener () {
        public void collectionChanged(CollectionEvent ev) {
          if (ev.getType() == CollectionEvent.Type.ADD) {
            ev.getItem().addPropertyChangeListener(furnitureChangeListener);
          } else if (ev.getType() == CollectionEvent.Type.DELETE) {
            ev.getItem().removePropertyChangeListener(furnitureChangeListener);
            furnitureSidesCache.remove(ev.getItem());
          }
        }
      });
    
    this.home.addPropertyChangeListener(Home.Property.SELECTED_LEVEL, new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          resetAreaCache();
        }
      });
    this.home.getObserverCamera().setFixedSize(home.getLevels().size() >= 2);
    this.home.addLevelsListener(new CollectionListener() {
        public void collectionChanged(CollectionEvent ev) {
          home.getObserverCamera().setFixedSize(home.getLevels().size() >= 2);
        }
      });
  }

  private void resetAreaCache() {
    wallsAreaCache = null;
    wallsIncludingBaseboardsAreaCache = null;
    insideWallsAreaCache = null;
    roomPathsCache = null;
  }
  
  /**
   * Displays in plan view the feedback of draggedItems,
   * during a drag and drop operation initiated from outside of plan view. 
   */
  public void startDraggedItems(List draggedItems, float x, float y) {
    this.draggedItems = draggedItems;
    // If magnetism is enabled, adjust furniture size and elevation
    if (this.preferences.isMagnetismEnabled()) {
      for (HomePieceOfFurniture piece : Home.getFurnitureSubList(draggedItems)) {
        if (piece.isResizable()) {
          piece.setWidth(this.preferences.getLengthUnit().getMagnetizedLength(piece.getWidth(), 0.1f));
          piece.setDepth(this.preferences.getLengthUnit().getMagnetizedLength(piece.getDepth(), 0.1f));
          piece.setHeight(this.preferences.getLengthUnit().getMagnetizedLength(piece.getHeight(), 0.1f));
        }
        piece.setElevation(this.preferences.getLengthUnit().getMagnetizedLength(piece.getElevation(), 0.1f));
      }
    }
    setState(getDragAndDropState());
    moveMouse(x, y);
  }

  /**
   * Deletes in plan view the feedback of the dragged items. 
   */
  public void stopDraggedItems() {
    if (this.state != getDragAndDropState()) {
      throw new IllegalStateException("Controller isn't in a drag and drop state");
    }
    this.draggedItems = null;    
    setState(this.previousState);
  }
  
  /**
   * Attempts to modify piece location depending of its context.
   * If the piece is a door or a window and the point (x, y)
   * belongs to a wall, the piece will be resized, rotated and moved so 
   * its opening depth is equal to wall thickness and its angle matches wall direction.
   * If the piece isn't a door or a window and the point (x, y)
   * belongs to a wall, the piece will be rotated and moved so 
   * its back face lies along the closest wall side and its angle matches wall direction.
   * If the piece isn't a door or a window, its bounding box is included in 
   * the one of an other object and its elevation is equal to zero, it will be elevated
   * to appear on the top of the latter. 
   */
  protected void adjustMagnetizedPieceOfFurniture(HomePieceOfFurniture piece, float x, float y) {
    boolean pieceElevationAdjusted = adjustPieceOfFurnitureElevation(piece) != null;
    Wall magnetWall = adjustPieceOfFurnitureOnWallAt(piece, x, y, true);
    if (!pieceElevationAdjusted) {
      adjustPieceOfFurnitureSideBySideAt(piece, magnetWall == null, magnetWall);
    }
  }
  
  /**
   * Attempts to move and resize piece depending on the wall under the 
   * point (x, y) and returns that wall it it exists.
   * @see #adjustMagnetizedPieceOfFurniture(HomePieceOfFurniture, float, float)
   */
  private Wall adjustPieceOfFurnitureOnWallAt(HomePieceOfFurniture piece, 
                                              float x, float y, boolean forceOrientation) {
    float margin = PIXEL_MARGIN / getScale();
    Level selectedLevel = this.home.getSelectedLevel();
    float [][] piecePoints = piece.getPoints();
    
    final boolean includeBaseboards = !piece.isDoorOrWindow()
        && piece.getElevation() == 0;
    Area wallsArea = getWallsArea(includeBaseboards);
    Collection walls = this.home.getWalls();
    
    Wall referenceWall = null;
    if (forceOrientation
        || !piece.isDoorOrWindow()) {
      // Search if point (x, y) is contained in home walls with no margin
      for (Wall wall : walls) {
        if (wall.isAtLevel(selectedLevel) 
            && isLevelNullOrViewable(wall.getLevel())
            && wall.containsPoint(x, y, includeBaseboards, 0) 
            && wall.getStartPointToEndPointDistance() > 0) {
          referenceWall = getReferenceWall(wall, x, y);
          break;
        }
      }
      if (referenceWall == null) {
        // If not found search if point (x, y) is contained in home walls with a margin
        for (Wall wall : walls) {
          if (wall.isAtLevel(selectedLevel)
              && isLevelNullOrViewable(wall.getLevel())
              && wall.containsPoint(x, y, includeBaseboards, 0) 
              && wall.getStartPointToEndPointDistance() > 0) {
            referenceWall = getReferenceWall(wall, x, y);
            break;
          }
        }
      }
    }

    if (referenceWall == null) {
      // Search if the border of a wall at floor level intersects with the given piece
      Area pieceAreaWithMargin = new Area(getRotatedRectangle(
          piece.getX() - piece.getWidth() / 2 - margin, piece.getY() - piece.getDepth() / 2 - margin, 
          piece.getWidth() + 2 * margin, piece.getDepth() + 2 * margin, piece.getAngle()));
      float intersectionWithReferenceWallSurface = 0;
      for (Wall wall : walls) {
        if (wall.isAtLevel(selectedLevel) 
            && isLevelNullOrViewable(wall.getLevel())
            && wall.getStartPointToEndPointDistance() > 0) {
          float [][] wallPoints = wall.getPoints(includeBaseboards);
          Area wallAreaIntersection = new Area(getPath(wallPoints));
          wallAreaIntersection.intersect(pieceAreaWithMargin);
          if (!wallAreaIntersection.isEmpty()) {
            float surface = getArea(wallAreaIntersection);
            if (surface > intersectionWithReferenceWallSurface) {
              intersectionWithReferenceWallSurface = surface;
              if (forceOrientation) {
                referenceWall = getReferenceWall(wall, x, y);
              } else {
                Rectangle2D intersectionBounds = wallAreaIntersection.getBounds2D();
                referenceWall = getReferenceWall(wall, (float)intersectionBounds.getCenterX(), (float)intersectionBounds.getCenterY());
              }
            }
          }
        }
      }
    }
    
    if (referenceWall != null
        && (referenceWall.getArcExtent() == null 
            || referenceWall.getArcExtent().floatValue() == 0 
            || !piece.isDoorOrWindow())) {
      float xPiece = x;
      float yPiece = y;
      float pieceAngle = piece.getAngle();      
      float halfWidth = piece.getWidth() / 2;
      float halfDepth = piece.getDepth() / 2;
      double wallAngle = Math.atan2(referenceWall.getYEnd() - referenceWall.getYStart(), 
          referenceWall.getXEnd() - referenceWall.getXStart());
      float [][] wallPoints = referenceWall.getPoints(includeBaseboards);
      boolean magnetizedAtRight = wallAngle > -Math.PI / 2 && wallAngle <= Math.PI / 2;
      double cosWallAngle = Math.cos(wallAngle);
      double sinWallAngle = Math.sin(wallAngle);
      double distanceToLeftSide = Line2D.ptLineDist(
          wallPoints [0][0], wallPoints [0][1], wallPoints [1][0], wallPoints [1][1], x, y);
      double distanceToRightSide = Line2D.ptLineDist(
          wallPoints [2][0], wallPoints [2][1], wallPoints [3][0], wallPoints [3][1], x, y);
      boolean adjustOrientation = forceOrientation
          || piece.isDoorOrWindow() 
          || referenceWall.containsPoint(x, y, includeBaseboards, PIXEL_MARGIN / getScale());
      if (adjustOrientation) {
        double distanceToPieceLeftSide = Line2D.ptLineDist(
            piecePoints [0][0], piecePoints [0][1], piecePoints [3][0], piecePoints [3][1], x, y);
        double distanceToPieceRightSide = Line2D.ptLineDist(
            piecePoints [1][0], piecePoints [1][1], piecePoints [2][0], piecePoints [2][1], x, y);
        double distanceToPieceSide = pieceAngle > (3 * Math.PI / 2 + 1E-6) || pieceAngle < (Math.PI / 2 + 1E-6)
            ? distanceToPieceLeftSide
            : distanceToPieceRightSide;
        pieceAngle = (float)(distanceToRightSide < distanceToLeftSide 
            ? wallAngle
            : wallAngle + Math.PI);

        if (piece.isDoorOrWindow()) {
          final float thicknessEpsilon = 0.00075f;
          float wallDistance = thicknessEpsilon / 2;
          if (piece instanceof HomeDoorOrWindow) {
            HomeDoorOrWindow doorOrWindow = (HomeDoorOrWindow)piece;
            if (piece.isResizable()
                && isItemResizable(piece)) {
              piece.setDepth(thicknessEpsilon 
                  + referenceWall.getThickness() / doorOrWindow.getWallThickness());
              halfDepth = piece.getDepth() / 2;
            }
            wallDistance += piece.getDepth() * doorOrWindow.getWallDistance();           
          } 
          if (distanceToRightSide < distanceToLeftSide) {
            xPiece += sinWallAngle * ( (distanceToLeftSide + wallDistance) - halfDepth);
            yPiece += cosWallAngle * (-(distanceToLeftSide + wallDistance) + halfDepth);
          } else {
            xPiece += sinWallAngle * (-(distanceToRightSide + wallDistance) + halfDepth);
            yPiece += cosWallAngle * ( (distanceToRightSide + wallDistance) - halfDepth);
          }
          if (magnetizedAtRight) {
            xPiece += cosWallAngle * (halfWidth - distanceToPieceSide);
            yPiece += sinWallAngle * (halfWidth - distanceToPieceSide);
          } else {
            // Ensure adjusted window is at the right of the cursor 
            xPiece += -cosWallAngle * (halfWidth - distanceToPieceSide);
            yPiece += -sinWallAngle * (halfWidth - distanceToPieceSide);
          }
        } else {
          if (distanceToRightSide < distanceToLeftSide) {
            int pointIndicator = Line2D.relativeCCW(
                wallPoints [2][0], wallPoints [2][1], wallPoints [3][0], wallPoints [3][1], x, y);
            xPiece +=  pointIndicator * sinWallAngle * distanceToRightSide - sinWallAngle * halfDepth;
            yPiece += -pointIndicator * cosWallAngle * distanceToRightSide + cosWallAngle * halfDepth;
          } else {
            int pointIndicator = Line2D.relativeCCW(
                wallPoints [0][0], wallPoints [0][1], wallPoints [1][0], wallPoints [1][1], x, y);
            xPiece += -pointIndicator * sinWallAngle * distanceToLeftSide + sinWallAngle * halfDepth;
            yPiece +=  pointIndicator * cosWallAngle * distanceToLeftSide - cosWallAngle * halfDepth;
          }
          if (magnetizedAtRight) {
            xPiece += cosWallAngle * (halfWidth - distanceToPieceSide);
            yPiece += sinWallAngle * (halfWidth - distanceToPieceSide);
          } else {
            // Ensure adjusted piece is at the right of the cursor 
            xPiece += -cosWallAngle * (halfWidth - distanceToPieceSide);
            yPiece += -sinWallAngle * (halfWidth - distanceToPieceSide);
          }
        }
      } else {
        // Search the distance required to align piece on the left or right side of the reference wall
        Line2D centerLine = new Line2D.Float(referenceWall.getXStart(), referenceWall.getYStart(), 
            referenceWall.getXEnd(), referenceWall.getYEnd());
        Shape pieceBoundingBox = getRotatedRectangle(0, 0, piece.getWidth(), piece.getDepth(), (float)(pieceAngle - wallAngle));
        double rotatedBoundingBoxDepth = pieceBoundingBox.getBounds2D().getHeight();
        float relativeCCWToPieceCenterSignum = Math.signum(centerLine.relativeCCW(piece.getX(), piece.getY()));
        float relativeCCWToPointSignum = Math.signum(centerLine.relativeCCW(x, y));
        double distance = relativeCCWToPieceCenterSignum 
            * (-referenceWall.getThickness() / 2 + centerLine.ptLineDist(piece.getX(), piece.getY()) - rotatedBoundingBoxDepth / 2);
        if (includeBaseboards) {
          if (relativeCCWToPieceCenterSignum > 0
              && referenceWall.getLeftSideBaseboard() != null) {
            distance -= relativeCCWToPieceCenterSignum * referenceWall.getLeftSideBaseboard().getThickness(); 
          } else if (relativeCCWToPieceCenterSignum < 0
                     && referenceWall.getRightSideBaseboard() != null) {
            distance -= relativeCCWToPieceCenterSignum * referenceWall.getRightSideBaseboard().getThickness(); 
          }
        }
        if (relativeCCWToPointSignum != relativeCCWToPieceCenterSignum) {
          distance -= relativeCCWToPointSignum * (rotatedBoundingBoxDepth + referenceWall.getThickness()); 
          if (referenceWall.getLeftSideBaseboard() != null) {
            distance -= relativeCCWToPointSignum * referenceWall.getLeftSideBaseboard().getThickness(); 
          } 
          if (referenceWall.getRightSideBaseboard() != null) {
            distance -= relativeCCWToPointSignum * referenceWall.getRightSideBaseboard().getThickness(); 
          }
        }
        xPiece = piece.getX() + (float)(-distance * sinWallAngle);
        yPiece = piece.getY() + (float)(distance * cosWallAngle);
      }
        
      if (!piece.isDoorOrWindow() 
          && (referenceWall.getArcExtent() == null // Ignore reoriented piece when (x, y) is inside a round wall
              || !adjustOrientation
              || Line2D.relativeCCW(referenceWall.getXStart(), referenceWall.getYStart(), 
                    referenceWall.getXEnd(), referenceWall.getYEnd(), x, y) > 0)) {
        // Search if piece intersects some other walls and avoid it intersects the closest one 
        Area wallsAreaIntersection = new Area(wallsArea);
        Area adjustedPieceArea = new Area(getRotatedRectangle(xPiece - halfWidth, 
                yPiece - halfDepth, piece.getWidth(), piece.getDepth(), pieceAngle));
        wallsAreaIntersection.subtract(new Area(getPath(wallPoints)));
        wallsAreaIntersection.intersect(adjustedPieceArea);
        if (!wallsAreaIntersection.isEmpty()) {
          // Search the wall intersection path the closest to (x, y)
          GeneralPath closestWallIntersectionPath = getClosestPath(getAreaPaths(wallsAreaIntersection), x, y);            
          if (closestWallIntersectionPath != null) {           
            // In case the adjusted piece crosses a wall, search the area intersecting that wall 
            // + other parts which crossed the wall (the farthest ones from (x,y))
            adjustedPieceArea.subtract(wallsArea); 
            if (adjustedPieceArea.isEmpty()) {
              return null;
            } else {
              List adjustedPieceAreaPaths = getAreaPaths(adjustedPieceArea);
              // Ignore too complex cases when the piece intersect many walls and is not parallel to a wall
              double angleDifference = (wallAngle - pieceAngle + 2 * Math.PI) % Math.PI;
              if (angleDifference < 1E-5 
                  || Math.PI - angleDifference < 1E-5
                  || adjustedPieceAreaPaths.size() < 2) {
                GeneralPath adjustedPiecePathInArea = getClosestPath(adjustedPieceAreaPaths, x, y);
                Area adjustingArea = new Area(closestWallIntersectionPath);
                for (GeneralPath path : adjustedPieceAreaPaths) {
                  if (path != adjustedPiecePathInArea) {
                    adjustingArea.add(new Area(path));
                  }
                }
                AffineTransform rotation = AffineTransform.getRotateInstance(-wallAngle);
                Rectangle2D adjustingAreaBounds = adjustingArea.createTransformedArea(rotation).getBounds2D();            
                Rectangle2D adjustedPiecePathInAreaBounds = adjustedPiecePathInArea.createTransformedShape(rotation).getBounds2D();
                if (!adjustingAreaBounds.contains(adjustedPiecePathInAreaBounds)) {
                  double adjustLeftBorder = Math.signum(adjustedPiecePathInAreaBounds.getCenterX() - adjustingAreaBounds.getCenterX());
                  xPiece += adjustingAreaBounds.getWidth() * cosWallAngle * adjustLeftBorder;
                  yPiece += adjustingAreaBounds.getWidth() * sinWallAngle * adjustLeftBorder;
                }
              }
            }
          }
        }
      }

      piece.setAngle(pieceAngle);
      piece.setX(xPiece);
      piece.setY(yPiece);
      if (piece instanceof HomeDoorOrWindow) {
        ((HomeDoorOrWindow)piece).setBoundToWall(true);
      }
      return referenceWall;
    } 
      
    return null;
  }

  /**
   * Returns true is the given level is viewable.
   */
  private boolean isLevelNullOrViewable(Level level) {
    return level == null || level.isViewable();
  }

  /**
   * Returns wall or a small wall part at the angle formed by the line joining wall center to 
   * (x, y) point if the given wall is round. 
   */
  private Wall getReferenceWall(Wall wall, float x, float y) {
    Float arcExtent = wall.getArcExtent();
    if (arcExtent == null || arcExtent.floatValue() == 0) {
      return wall;
    } else {
      double angle = Math.atan2(wall.getYArcCircleCenter() - y, x - wall.getXArcCircleCenter());
      double radius = Point2D.distance(wall.getXArcCircleCenter(), wall.getYArcCircleCenter(), wall.getXStart(), wall.getYStart());
      float epsilonAngle = 0.001f;
      Wall wallPart = new Wall((float)(wall.getXArcCircleCenter() + Math.cos(angle + epsilonAngle) * radius), 
          (float)(wall.getYArcCircleCenter() - Math.sin(angle + epsilonAngle) * radius),
          (float)(wall.getXArcCircleCenter() + Math.cos(angle - epsilonAngle) * radius), 
          (float)(wall.getYArcCircleCenter() - Math.sin(angle - epsilonAngle) * radius), wall.getThickness(), 0);
      wallPart.setArcExtent(epsilonAngle * 2);
      wallPart.setLeftSideBaseboard(wall.getLeftSideBaseboard());
      wallPart.setRightSideBaseboard(wall.getRightSideBaseboard());
      return wallPart;
    }
  }

  /**
   * Returns the closest path among paths ones to the given point.
   */
  private GeneralPath getClosestPath(List paths, float x, float y) {
    GeneralPath closestPath = null;
    double closestPathDistance = Double.MAX_VALUE;
    for (GeneralPath path : paths) {
      float [][] pathPoints = getPathPoints(path, true);
      for (int i = 0; i < pathPoints.length; i++) {
        double distanceToPath = Line2D.ptSegDistSq(pathPoints [i][0], pathPoints [i][1], 
            pathPoints [(i + 1) % pathPoints.length][0], pathPoints [(i + 1) % pathPoints.length][1], x, y);
        if (distanceToPath < closestPathDistance) {
          closestPathDistance = distanceToPath;
          closestPath = path;
        }
      }
    }
    return closestPath;
  }

  /**
   * Returns the dimension lines that indicates how is placed a given piece
   * along a wall. 
   */
  private List getDimensionLinesAlongWall(HomePieceOfFurniture piece, Wall wall) {
    // Search the points on the wall side closest to piece
    float [][] piecePoints = piece.getPoints();
    float angle = piece.getAngle();
    float [][] wallPoints = wall.getPoints();
    float [] pieceLeftPoint;
    float [] pieceRightPoint;
    float [] piecePoint = piece.isDoorOrWindow()
        ? piecePoints [3] // Front side point
        : piecePoints [0]; // Back side point
    if (Line2D.ptLineDistSq(wallPoints [0][0], wallPoints [0][1], 
            wallPoints [1][0], wallPoints [1][1], 
            piecePoint [0], piecePoint [1]) 
        <= Line2D.ptLineDistSq(wallPoints [2][0], wallPoints [2][1],
            wallPoints [3][0], wallPoints [3][1], 
            piecePoint [0], piecePoint [1])) {
      pieceLeftPoint = computeIntersection(wallPoints [0], wallPoints [1], piecePoints [0], piecePoints [3]);
      pieceRightPoint = computeIntersection(wallPoints [0], wallPoints [1], piecePoints [1], piecePoints [2]);
    } else {
      pieceLeftPoint = computeIntersection(wallPoints [2], wallPoints [3], piecePoints [0], piecePoints [3]);
      pieceRightPoint = computeIntersection(wallPoints [2], wallPoints [3], piecePoints [1], piecePoints [2]);
    }
    
    List dimensionLines = new ArrayList();
    float [] wallEndPointJoinedToPieceLeftPoint = null;
    float [] wallEndPointJoinedToPieceRightPoint = null;
    // Search among room paths which segment includes pieceLeftPoint and pieceRightPoint
    List roomPaths = getRoomPathsFromWalls();
    for (int i = 0; 
         i < roomPaths.size()
         && wallEndPointJoinedToPieceLeftPoint == null 
         && wallEndPointJoinedToPieceRightPoint == null; i++) {
      float [][] roomPoints = getPathPoints(roomPaths.get(i), true);
      for (int j = 0; j < roomPoints.length; j++) {
        float [] startPoint = roomPoints [j];
        float [] endPoint = roomPoints [(j + 1) % roomPoints.length];
        float deltaX = endPoint [0] - startPoint [0];
        float deltaY = endPoint [1] - startPoint [1];
        double segmentAngle = Math.abs(deltaX) < 1E-5
            ? Math.PI / 2
            : (Math.abs(deltaY) < 1E-5
                  ? 0
                  : Math.atan2(deltaY, deltaX));
        // If segment and piece are parallel
        double angleDifference = (segmentAngle - angle + 2 * Math.PI) % Math.PI;
        if (angleDifference < 1E-5 || Math.PI - angleDifference < 1E-5) {
          boolean segmentContainsLeftPoint = Line2D.ptSegDistSq(startPoint [0], startPoint [1], 
              endPoint [0], endPoint [1], pieceLeftPoint [0], pieceLeftPoint [1]) < 0.0001;
          boolean segmentContainsRightPoint = Line2D.ptSegDistSq(startPoint [0], startPoint [1], 
              endPoint [0], endPoint [1], pieceRightPoint [0], pieceRightPoint [1]) < 0.0001;
          if (segmentContainsLeftPoint || segmentContainsRightPoint) {
            if (segmentContainsLeftPoint) {
              // Compute distances to segment start point
              double startPointToLeftPointDistance = Point2D.distanceSq(startPoint [0], startPoint [1], 
                  pieceLeftPoint [0], pieceLeftPoint [1]);
              double startPointToRightPointDistance = Point2D.distanceSq(startPoint [0], startPoint [1], 
                  pieceRightPoint [0], pieceRightPoint [1]);
              if (startPointToLeftPointDistance < startPointToRightPointDistance
                  || !segmentContainsRightPoint) {
                wallEndPointJoinedToPieceLeftPoint = startPoint.clone();
              } else {
                wallEndPointJoinedToPieceLeftPoint = endPoint.clone();
              }
            }
            if (segmentContainsRightPoint) {
              // Compute distances to segment start point
              double endPointToLeftPointDistance = Point2D.distanceSq(endPoint [0], endPoint [1], 
                  pieceLeftPoint [0], pieceLeftPoint [1]);
              double endPointToRightPointDistance = Point2D.distanceSq(endPoint [0], endPoint [1], 
                  pieceRightPoint [0], pieceRightPoint [1]);
              if (endPointToLeftPointDistance < endPointToRightPointDistance
                  && segmentContainsLeftPoint) {
                wallEndPointJoinedToPieceRightPoint = startPoint.clone();
              } else {
                wallEndPointJoinedToPieceRightPoint = endPoint.clone();
              }
            }
            break;
          }
        }
      }
    }

    boolean reverse = angle > Math.PI / 2 && angle <= 3 * Math.PI / 2;
    boolean pieceFrontSideAlongWallSide = !piece.isDoorOrWindow() 
        && Line2D.ptLineDistSq(wall.getXStart(), wall.getYStart(), wall.getXEnd(), wall.getYEnd(), piecePoint [0], piecePoint [1]) 
            > Line2D.ptLineDistSq(wall.getXStart(), wall.getYStart(), wall.getXEnd(), wall.getYEnd(), piecePoints [3][0], piecePoints [3][1]);
    if (wallEndPointJoinedToPieceLeftPoint != null) {
      float offset = (float)Point2D.distance(pieceLeftPoint [0], pieceLeftPoint [1], 
          piecePoints [3][0], piecePoints [3][1]) + 10 / getView().getScale();
      if (pieceFrontSideAlongWallSide) {
        offset = -(float)Point2D.distance(pieceLeftPoint [0], pieceLeftPoint [1], 
            piecePoints [0][0], piecePoints [0][1]) - 10 / getView().getScale();
      }
      if (reverse) {
        dimensionLines.add(new DimensionLine(pieceLeftPoint [0], pieceLeftPoint [1],
            wallEndPointJoinedToPieceLeftPoint [0],
            wallEndPointJoinedToPieceLeftPoint [1], -offset));
      } else {
        dimensionLines.add(new DimensionLine(wallEndPointJoinedToPieceLeftPoint [0],
            wallEndPointJoinedToPieceLeftPoint [1], 
            pieceLeftPoint [0], pieceLeftPoint [1], offset));
      }
    }
    if (wallEndPointJoinedToPieceRightPoint != null) {
      float offset = (float)Point2D.distance(pieceRightPoint [0], pieceRightPoint [1], 
          piecePoints [2][0], piecePoints [2][1]) + 10 / getView().getScale();
      if (pieceFrontSideAlongWallSide) {
        offset = -(float)Point2D.distance(pieceRightPoint [0], pieceRightPoint [1], 
            piecePoints [1][0], piecePoints [1][1]) - 10 / getView().getScale();
      }
      if (reverse) {
        dimensionLines.add(new DimensionLine(wallEndPointJoinedToPieceRightPoint [0],
            wallEndPointJoinedToPieceRightPoint [1], 
            pieceRightPoint [0], pieceRightPoint [1], -offset));
      } else {
        dimensionLines.add(new DimensionLine(pieceRightPoint [0], pieceRightPoint [1],
            wallEndPointJoinedToPieceRightPoint [0],
            wallEndPointJoinedToPieceRightPoint [1], offset));
      }
    }
    for (Iterator it = dimensionLines.iterator(); it.hasNext(); ) {
      if (it.next().getLength() < 0.01f) {
        it.remove();
      }
    }
    return dimensionLines;
  }

  /** 
   * Returns the intersection point between the lines defined by the points
   * (point1, point2) and (point3, pont4).
   */
  private static float [] computeIntersection(float [] point1, float [] point2, float [] point3, float [] point4) {
    return computeIntersection(point1 [0], point1 [1], point2 [0], point2 [1], 
        point3 [0], point3 [1], point4 [0], point4 [1]);
  }
  
  /** 
   * Returns the intersection point between the line joining the first two points and
   * the line joining the two last points.
   */
  static float [] computeIntersection(float xPoint1, float yPoint1, float xPoint2, float yPoint2, 
                                      float xPoint3, float yPoint3, float xPoint4, float yPoint4) {
    float x = xPoint2;
    float y = yPoint2;
    float alpha1 = (yPoint2 - yPoint1) / (xPoint2 - xPoint1);
    float alpha2 = (yPoint4 - yPoint3) / (xPoint4 - xPoint3);
    // If the two lines are not parallel
    if (alpha1 != alpha2) {
      // If first line is vertical
      if (Math.abs(alpha1) > 4000)  {
        if (Math.abs(alpha2) < 4000) {
          x = xPoint1;
          float beta2  = yPoint4 - alpha2 * xPoint4;
          y = alpha2 * x + beta2;
        }
      // If second line is vertical
      } else if (Math.abs(alpha2) > 4000) {
        if (Math.abs(alpha1) < 4000) {
          x = xPoint3;
          float beta1  = yPoint2 - alpha1 * xPoint2;
          y = alpha1 * x + beta1;
        }
      } else {
        boolean sameSignum = Math.signum(alpha1) == Math.signum(alpha2);
        if (Math.abs(alpha1 - alpha2) > 1E-5
            && (!sameSignum || (Math.abs(alpha1) > Math.abs(alpha2)   ? alpha1 / alpha2   : alpha2 / alpha1) > 1.004)) {
          float beta1  = yPoint2 - alpha1 * xPoint2;
          float beta2  = yPoint4 - alpha2 * xPoint4;
          x = (beta2 - beta1) / (alpha1 - alpha2);
          y = alpha1 * x + beta1;
        }
      } 
    }
    return new float [] {x, y};
  }
  
  /**
   * Attempts to elevate piece depending on the highest piece that includes
   * its bounding box and returns that piece.
   * @see #adjustMagnetizedPieceOfFurniture(HomePieceOfFurniture, float, float)
   */
  private HomePieceOfFurniture adjustPieceOfFurnitureElevation(HomePieceOfFurniture piece) {
    if (!piece.isDoorOrWindow()
        && piece.getElevation() == 0) {
      // Search if another piece at floor level contains the given piece to elevate it at its height
      HomePieceOfFurniture highestSurroundingPiece = getHighestSurroundingPieceOfFurniture(piece);
      if (highestSurroundingPiece != null) {
        float elevation = highestSurroundingPiece.getElevation() 
                + highestSurroundingPiece.getHeight() * highestSurroundingPiece.getDropOnTopElevation();
        if (highestSurroundingPiece.getLevel() != null) {
          elevation += highestSurroundingPiece.getLevel().getElevation() 
              - (piece.getLevel() != null 
                    ? piece.getLevel().getElevation()
                    : this.home.getSelectedLevel().getElevation());
        }
        piece.setElevation(Math.max(0, elevation));
        return highestSurroundingPiece;
      }
    }
    return null;
  }

  /**
   * Attempts to align piece on the borders of home furniture at the the same elevation 
   * that intersect with it and returns that piece.
   * @see #adjustMagnetizedPieceOfFurniture(HomePieceOfFurniture, float, float)
   */
  private HomePieceOfFurniture adjustPieceOfFurnitureSideBySideAt(HomePieceOfFurniture piece, 
                                                                  boolean forceOrientation, 
                                                                  Wall magnetWall) {
    float [][] piecePoints = piece.getPoints();
    Area pieceArea = new Area(getPath(piecePoints));
    boolean doorOrWindowBoundToWall = piece instanceof HomeDoorOrWindow 
        && ((HomeDoorOrWindow)piece).isBoundToWall();
    
    // Search if the border of another piece at floor level intersects with the given piece
    float pieceElevation = piece.getGroundElevation();
    float margin = 2 * PIXEL_MARGIN / getScale();
    BasicStroke stroke = new BasicStroke(margin);
    HomePieceOfFurniture referencePiece = null;
    Area intersectionWithReferencePieceArea = null;
    float intersectionWithReferencePieceSurface = 0;
    float [][] referencePiecePoints = null;
    for (HomePieceOfFurniture homePiece : this.home.getFurniture()) {
      float homePieceElevation = homePiece.getGroundElevation();
      if (homePiece != piece 
          && isPieceOfFurnitureVisibleAtSelectedLevel(homePiece)
          && pieceElevation < homePieceElevation + homePiece.getHeight()
          && pieceElevation + piece.getHeight() > homePieceElevation
          && (!doorOrWindowBoundToWall // Ignore other furniture for doors and windows bound to a wall
              || homePiece.isDoorOrWindow())) {
        float [][] points = homePiece.getPoints();
        GeneralPath path = getPath(points);
        Area marginArea;
        if (doorOrWindowBoundToWall && homePiece.isDoorOrWindow()) {
          marginArea = new Area(stroke.createStrokedShape(new Line2D.Float(
              points [1][0], points [1][1], points [2][0], points [2][1])));
          marginArea.add(new Area(stroke.createStrokedShape(new Line2D.Float(
              points [3][0], points [3][1], points [0][0], points [0][1]))));
        } else {
          marginArea = this.furnitureSidesCache.get(homePiece);
          if (marginArea == null) {
            marginArea = new Area(stroke.createStrokedShape(path));
            this.furnitureSidesCache.put(homePiece, marginArea);
          }
        }
        Area intersection = new Area(marginArea);
        intersection.intersect(pieceArea);
        if (!intersection.isEmpty()) {
          Area exclusiveOr = new Area(pieceArea);
          exclusiveOr.exclusiveOr(intersection);
          if (exclusiveOr.isSingular()) {
            Area insideArea = new Area(path);
            insideArea.subtract(marginArea);
            insideArea.intersect(pieceArea);
            if (insideArea.isEmpty()) {
              float surface = getArea(intersection);
              if (surface > intersectionWithReferencePieceSurface) {
                intersectionWithReferencePieceSurface = surface;
                referencePiece = homePiece;
                referencePiecePoints = points;
                intersectionWithReferencePieceArea = intersection;
              }
            }
          }
        }
      }
    }
    
    if (referencePiece != null) {
      boolean alignedOnReferencePieceFrontOrBackSide;
      if (doorOrWindowBoundToWall && referencePiece.isDoorOrWindow()) {
        alignedOnReferencePieceFrontOrBackSide = false;
      } else {
        GeneralPath referencePieceLargerBoundingBox = getRotatedRectangle(referencePiece.getX() - referencePiece.getWidth(), 
            referencePiece.getY() - referencePiece.getDepth(), referencePiece.getWidth() * 2, referencePiece.getDepth() * 2, 
            referencePiece.getAngle());
        float [][] pathPoints = getPathPoints(referencePieceLargerBoundingBox, false);
        alignedOnReferencePieceFrontOrBackSide = isAreaLargerOnFrontOrBackSide(intersectionWithReferencePieceArea, pathPoints);
      }
      if (forceOrientation) {  
        piece.setAngle(referencePiece.getAngle());
      }
      Shape pieceBoundingBox = getRotatedRectangle(0, 0, piece.getWidth(), piece.getDepth(), piece.getAngle() - referencePiece.getAngle());
      float deltaX = 0;
      float deltaY = 0;
      if (!alignedOnReferencePieceFrontOrBackSide) {
        // Search the distance required to align piece on the left or right side of the reference piece
        Line2D centerLine = new Line2D.Float(referencePiece.getX(), referencePiece.getY(), 
            (referencePiecePoints [0][0] + referencePiecePoints [1][0]) / 2, (referencePiecePoints [0][1] + referencePiecePoints [1][1]) / 2);
        double rotatedBoundingBoxWidth = pieceBoundingBox.getBounds2D().getWidth();
        double distance = centerLine.relativeCCW(piece.getX(), piece.getY()) 
            * (-referencePiece.getWidth() / 2 + centerLine.ptLineDist(piece.getX(), piece.getY()) - rotatedBoundingBoxWidth / 2);      
        deltaX = (float)(distance * Math.cos(referencePiece.getAngle()));
        deltaY = (float)(distance * Math.sin(referencePiece.getAngle()));
      } else {
        // Search the distance required to align piece on the front or back side of the reference piece
        Line2D centerLine = new Line2D.Float(referencePiece.getX(), referencePiece.getY(), 
            (referencePiecePoints [2][0] + referencePiecePoints [1][0]) / 2, (referencePiecePoints [2][1] + referencePiecePoints [1][1]) / 2);
        double rotatedBoundingBoxDepth = pieceBoundingBox.getBounds2D().getHeight();
        double distance = centerLine.relativeCCW(piece.getX(), piece.getY()) 
            * (-referencePiece.getDepth() / 2 + centerLine.ptLineDist(piece.getX(), piece.getY()) - rotatedBoundingBoxDepth / 2);      
        deltaX = (float)(-distance * Math.sin(referencePiece.getAngle()));
        deltaY = (float)(distance * Math.cos(referencePiece.getAngle()));
        if (!isIntersectionEmpty(piece, magnetWall, deltaX, deltaY)) {
          deltaX = deltaY = 0;
        }
      }
      
      // Accept move only if reference piece and moved piece share some points
      if (!isIntersectionEmpty(piece, referencePiece, deltaX, deltaY)) {
        piece.move(deltaX, deltaY);
        return referencePiece;
      } else {
        if (forceOrientation) {  
          // Update points array
          piecePoints = piece.getPoints();
        }
        boolean alignedOnPieceFrontOrBackSide = isAreaLargerOnFrontOrBackSide(intersectionWithReferencePieceArea, piecePoints);        
        Shape referencePieceBoundingBox = getRotatedRectangle(0, 0, referencePiece.getWidth(), referencePiece.getDepth(), 
            referencePiece.getAngle() - piece.getAngle());        
        if (!alignedOnPieceFrontOrBackSide) {
          // Search the distance required to align piece on its left or right side 
          Line2D centerLine = new Line2D.Float(piece.getX(), piece.getY(), 
              (piecePoints [0][0] + piecePoints [1][0]) / 2, (piecePoints [0][1] + piecePoints [1][1]) / 2);
          double rotatedBoundingBoxWidth = referencePieceBoundingBox.getBounds2D().getWidth();
          double distance = centerLine.relativeCCW(referencePiece.getX(), referencePiece.getY()) 
              * (-piece.getWidth() / 2 + centerLine.ptLineDist(referencePiece.getX(), referencePiece.getY()) - rotatedBoundingBoxWidth / 2);      
          deltaX = -(float)(distance * Math.cos(piece.getAngle()));
          deltaY = -(float)(distance * Math.sin(piece.getAngle()));
        } else {
          // Search the distance required to align piece on its front or back side 
          Line2D centerLine = new Line2D.Float(piece.getX(), piece.getY(), 
              (piecePoints [2][0] + piecePoints [1][0]) / 2, (piecePoints [2][1] + piecePoints [1][1]) / 2);
          double rotatedBoundingBoxDepth = referencePieceBoundingBox.getBounds2D().getHeight();
          double distance = centerLine.relativeCCW(referencePiece.getX(), referencePiece.getY()) 
              * (-piece.getDepth() / 2 + centerLine.ptLineDist(referencePiece.getX(), referencePiece.getY()) - rotatedBoundingBoxDepth / 2);      
          deltaX = -(float)(-distance * Math.sin(piece.getAngle()));
          deltaY = -(float)(distance * Math.cos(piece.getAngle()));
          if (!isIntersectionEmpty(piece, magnetWall, deltaX, deltaY)) {
            deltaX = deltaY = 0;
          }
        }

        // Accept move only if reference piece and moved piece share some points
        if (!isIntersectionEmpty(piece, referencePiece, deltaX, deltaY)) {
          piece.move(deltaX, deltaY);
          return referencePiece;
        }
      }
      return referencePiece;
    }
    return null;
  }

  /**
   * Returns true if the intersection between the given area and 
   * the front or back sides of the rectangle defined by piecePoints is larger
   * than with the left and right sides of this rectangle.
   */
  private boolean isAreaLargerOnFrontOrBackSide(Area area, float [][] piecePoints) {
    GeneralPath pieceFrontAndBackQuarters = new GeneralPath();
    pieceFrontAndBackQuarters.moveTo(piecePoints [0][0], piecePoints [0][1]);
    pieceFrontAndBackQuarters.lineTo(piecePoints [2][0], piecePoints [2][1]);
    pieceFrontAndBackQuarters.lineTo(piecePoints [3][0], piecePoints [3][1]);
    pieceFrontAndBackQuarters.lineTo(piecePoints [1][0], piecePoints [1][1]);
    pieceFrontAndBackQuarters.closePath();
    Area intersectionWithFrontOrBack = new Area(area);
    intersectionWithFrontOrBack.intersect(new Area(pieceFrontAndBackQuarters));
    if (intersectionWithFrontOrBack.isEmpty()) {
      return false;
    } else {
      GeneralPath pieceLeftAndRightQuarters = new GeneralPath();
      pieceLeftAndRightQuarters.moveTo(piecePoints [0][0], piecePoints [0][1]);
      pieceLeftAndRightQuarters.lineTo(piecePoints [2][0], piecePoints [2][1]);
      pieceLeftAndRightQuarters.lineTo(piecePoints [1][0], piecePoints [1][1]);
      pieceLeftAndRightQuarters.lineTo(piecePoints [3][0], piecePoints [3][1]);
      pieceLeftAndRightQuarters.closePath();
      Area intersectionWithLeftAndRight = new Area(area);      
      intersectionWithLeftAndRight.intersect(new Area(pieceLeftAndRightQuarters));
      return getArea(intersectionWithFrontOrBack) > getArea(intersectionWithLeftAndRight);
    }
  }

  /**
   * Returns the area of the given shape.
   */
  private float getArea(Area area) {
    float [][] pathPoints = getPathPoints(getPath(area), false);
    if (pathPoints.length > 1) {
      return new Room(pathPoints).getArea();
    } else {
      return 0;
    }
  }

  /**
   * Returns true if the given pieces don't intersect once the first is moved from 
   * (deltaX, deltaY) vector.
   */
  private boolean isIntersectionEmpty(HomePieceOfFurniture piece1, HomePieceOfFurniture piece2,
                                      float deltaX, float deltaY) {
    Area intersection = new Area(getRotatedRectangle(piece1.getX() - piece1.getWidth() / 2 + deltaX, 
        piece1.getY() - piece1.getDepth() / 2 + deltaY, piece1.getWidth(), piece1.getDepth(), piece1.getAngle()));
    float epsilon = 0.01f;
    intersection.intersect(new Area(getRotatedRectangle(piece2.getX() - piece2.getWidth() / 2 - epsilon, 
        piece2.getY() - piece2.getDepth() / 2 - epsilon, 
        piece2.getWidth() + 2 * epsilon, piece2.getDepth() + 2 * epsilon, piece2.getAngle())));
    return intersection.isEmpty();
  }
  
  /**
   * Returns true if the given area and wall don't intersect once the area is moved from 
   * (deltaX, deltaY) vector.
   */
  private boolean isIntersectionEmpty(HomePieceOfFurniture piece, Wall wall,
                                      float deltaX, float deltaY) {
    if (wall != null) {
      Area wallAreaIntersection = new Area(getPath(wall.getPoints()));
      wallAreaIntersection.intersect(new Area(getRotatedRectangle(piece.getX() - piece.getWidth() / 2 + deltaX, 
          piece.getY() - piece.getDepth() / 2 + deltaY, piece.getWidth(), piece.getDepth(), piece.getAngle())));
      return getArea(wallAreaIntersection) < 1E-4f;
    }
    return true;
  }
  
  /**
   * Returns the shape of the given rectangle rotated of a given angle. 
   */
  private GeneralPath getRotatedRectangle(float x, float y, float width, float height, float angle) {
    Rectangle2D referencePieceLargerBoundingBox = new Rectangle2D.Float(x, y, width, height); 
    AffineTransform rotation = AffineTransform.getRotateInstance(angle, x + width / 2, y + height / 2);
    GeneralPath rotatedBoundingBox = new GeneralPath();
    rotatedBoundingBox.append(referencePieceLargerBoundingBox.getPathIterator(rotation), false);
    return rotatedBoundingBox;
  }
  
  /**
   * Returns the dimension line that measures the side of a piece, the length of a room side 
   * or the length of a wall side at (x, y) point,
   * or null if it doesn't exist. 
   */
  private DimensionLine getMeasuringDimensionLineAt(float x, float y, 
                                                    boolean magnetismEnabled) {
    float margin = PIXEL_MARGIN / getScale();
    for (HomePieceOfFurniture piece : this.home.getFurniture()) {
      if (isPieceOfFurnitureVisibleAtSelectedLevel(piece)) {
        DimensionLine dimensionLine = getDimensionLineBetweenPointsAt(piece.getPoints(), x, y, margin, magnetismEnabled);
        if (dimensionLine != null) {
          return dimensionLine;
        }
      }
    }
    for (GeneralPath roomPath : getRoomPathsFromWalls()) {
      if (roomPath.intersects(x - margin, y - margin, 2 * margin, 2 * margin)) {
        DimensionLine dimensionLine = getDimensionLineBetweenPointsAt(
            getPathPoints(roomPath, true), x, y, margin, magnetismEnabled);
        if (dimensionLine != null) {
          return dimensionLine;
        }
      }
    }
    for (Room room : this.home.getRooms()) {
      if (isLevelNullOrViewable(room.getLevel()) 
          && room.isAtLevel(this.home.getSelectedLevel())) {
        DimensionLine dimensionLine = getDimensionLineBetweenPointsAt(room.getPoints(), x, y, margin, magnetismEnabled);
        if (dimensionLine != null) {
          return dimensionLine;
        }
      }
    }
    return null;
  }

  /**
   * Returns the dimension line that measures the side of the given polygon at (x, y) point,
   * or null if it doesn't exist. 
   */
  private DimensionLine getDimensionLineBetweenPointsAt(float [][] points, float x, float y, 
                                                        float margin, boolean magnetismEnabled) {
    for (int i = 0; i < points.length; i++) {
      int nextPointIndex = (i + 1) % points.length;
      // Ignore sides with a length smaller than 0.1 cm
      double distanceBetweenPointsSq = Point2D.distanceSq(points [i][0], points [i][1], 
              points [nextPointIndex][0], points [nextPointIndex][1]);
      if (distanceBetweenPointsSq > 0.01
          && Line2D.ptSegDistSq(points [i][0], points [i][1], 
              points [nextPointIndex][0], points [nextPointIndex][1], 
              x, y) <= margin * margin) {
        double angle = Math.atan2(points [i][1] - points [nextPointIndex][1], 
            points [nextPointIndex][0] - points [i][0]);
        boolean reverse = angle < -Math.PI / 2 || angle > Math.PI / 2;
        
        float xStart;
        float yStart;
        float xEnd;
        float yEnd;
        if (reverse) {
          // Avoid reversed text on the dimension line
          xStart = points [nextPointIndex][0];
          yStart = points [nextPointIndex][1];
          xEnd = points [i][0];
          yEnd = points [i][1];
        } else {
          xStart = points [i][0];
          yStart = points [i][1];
          xEnd = points [nextPointIndex][0];
          yEnd = points [nextPointIndex][1];
        }
        
        if (magnetismEnabled) {
          float magnetizedLength = this.preferences.getLengthUnit().getMagnetizedLength(
              (float)Math.sqrt(distanceBetweenPointsSq), getView().getPixelLength());
          if (reverse) {
            xEnd = points [nextPointIndex][0] - (float)(magnetizedLength * Math.cos(angle));            
            yEnd = points [nextPointIndex][1] + (float)(magnetizedLength * Math.sin(angle));
          } else {
            xEnd = points [i][0] + (float)(magnetizedLength * Math.cos(angle));            
            yEnd = points [i][1] - (float)(magnetizedLength * Math.sin(angle));
          }
        }
        return new DimensionLine(xStart, yStart, xEnd, yEnd, 0);
      }
    }
    return null;
  }

  /**
   * Controls the creation of a new level.
   */
  public void addLevel() {
    addLevel(false);
  }
  
  /**
   * Controls the creation of a new level at same elevation.
   */
  public void addLevelAtSameElevation() {
    addLevel(true);
  }
    
  /**
   * Controls the creation of a level.
   */
  private void addLevel(boolean sameElevation) {
    final boolean allLevelsSelection = this.home.isAllLevelsSelection();
    List oldSelection = this.home.getSelectedItems();
    final Selectable [] oldSelectedItems = 
        oldSelection.toArray(new Selectable [oldSelection.size()]);
    final Level oldSelectedLevel = this.home.getSelectedLevel();
    final BackgroundImage homeBackgroundImage = this.home.getBackgroundImage();
    List levels = this.home.getLevels();
    float newWallHeight = this.preferences.getNewWallHeight();
    float newFloorThickness = this.preferences.getNewFloorThickness();
    final Level level0;
    if (levels.isEmpty()) {
      // Create level 0
      String level0Name = this.preferences.getLocalizedString(PlanController.class, "levelName", 0);
      level0 = createLevel(level0Name, 0, newFloorThickness, newWallHeight);
      moveHomeItemsToLevel(level0);
      level0.setBackgroundImage(homeBackgroundImage);
      this.home.setBackgroundImage(null);
      levels = this.home.getLevels();
    } else {
      level0 = null;
    }
    String newLevelName = this.preferences.getLocalizedString(PlanController.class, "levelName", levels.size());
    final Level newLevel;
    if (sameElevation) {
      Level referencedLevel = level0 != null
          ? level0
          : home.getSelectedLevel();
      newLevel = createLevel(newLevelName, referencedLevel.getElevation(), 
          referencedLevel.getFloorThickness(), referencedLevel.getHeight());
    } else {
      float newLevelElevation = levels.get(levels.size() - 1).getElevation() 
          + newWallHeight + newFloorThickness;
      newLevel = createLevel(newLevelName, newLevelElevation, newFloorThickness, newWallHeight);
    }
    setSelectedLevel(newLevel);
    UndoableEdit undoableEdit = new AbstractUndoableEdit() {      
      @Override
      public void undo() throws CannotUndoException {
        super.undo();
        setSelectedLevel(oldSelectedLevel);
        home.deleteLevel(newLevel);
        if (level0 != null) {
          home.setBackgroundImage(homeBackgroundImage);
          moveHomeItemsToLevel(oldSelectedLevel);
          home.deleteLevel(level0);
        }
        selectAndShowItems(Arrays.asList(oldSelectedItems), allLevelsSelection);
      }
      
      @Override
      public void redo() throws CannotRedoException {
        super.redo();
        if (level0 != null) {
          home.addLevel(level0);
          moveHomeItemsToLevel(level0);
          level0.setBackgroundImage(homeBackgroundImage);
          home.setBackgroundImage(null);
        }
        home.addLevel(newLevel);
        setSelectedLevel(newLevel);
      }      

      @Override
      public String getPresentationName() {
        return preferences.getLocalizedString(PlanController.class, "undoAddLevel");
      }      
    };
    this.undoSupport.postEdit(undoableEdit);
  }

  /**
   * Returns a new level added to home.  
   */
  protected Level createLevel(String name, float elevation, float floorThickness, float height) {
    Level newLevel = new Level(name, elevation, floorThickness, height);
    this.home.addLevel(newLevel);
    return newLevel;
  }

  /**
   * Moves to the given level all existing furniture, walls, rooms, dimension lines 
   * and labels. 
   */
  private void moveHomeItemsToLevel(Level level) {
    for (HomePieceOfFurniture piece : this.home.getFurniture()) {
      piece.setLevel(level);
    }
    for (Wall wall : this.home.getWalls()) {
      wall.setLevel(level);
    }
    for (Room room : this.home.getRooms()) {
      room.setLevel(level);
    }
    for (Polyline polyline : this.home.getPolylines()) {
      polyline.setLevel(level);
    }
    for (DimensionLine dimensionLine : this.home.getDimensionLines()) {
      dimensionLine.setLevel(level);
    }
    for (Label label : this.home.getLabels()) {
      label.setLevel(level);
    }
  }

  /**
   * Toggles the viewability of the selected level.
   * @since 5.0
   */
  public void toggleSelectedLevelViewability() {
    final Level selectedLevel = this.home.getSelectedLevel();
    selectedLevel.setViewable(!selectedLevel.isViewable());
    undoSupport.postEdit(new AbstractUndoableEdit() {
        @Override
        public void undo() throws CannotUndoException {
          super.undo();
          setSelectedLevel(selectedLevel);
          selectedLevel.setViewable(!selectedLevel.isViewable());
        }
        
        @Override
        public void redo() throws CannotRedoException {
          super.redo();
          setSelectedLevel(selectedLevel);
          selectedLevel.setViewable(!selectedLevel.isViewable());
        }
  
        @Override
        public String getPresentationName() {
          return preferences.getLocalizedString(PlanController.class, "undoModifyLevelName");
        }      
      });
  }
  
  public void modifySelectedLevel() {
    if (this.home.getSelectedLevel() != null) {
      new LevelController(this.home, this.preferences, this.viewFactory,
          this.undoSupport).displayView(getView());
    }
  }
  
  /**
   * Deletes the selected level and the items that belongs to it.
   */
  public void deleteSelectedLevel() {
    // Start a compound edit that delete walls, furniture, rooms, dimension lines and labels from home
    undoSupport.beginUpdate();
    List levelFurniture = new ArrayList();
    final Level oldSelectedLevel = this.home.getSelectedLevel();
    for (HomePieceOfFurniture piece : this.home.getFurniture()) {
      if (piece.getLevel() == oldSelectedLevel) {
        levelFurniture.add(piece);
      }
    }
    // Delete furniture with inherited method
    deleteFurniture(levelFurniture);      
    
    List levelOtherItems = new ArrayList();
    addLevelItemsAtSelectedLevel(this.home.getWalls(), levelOtherItems);
    addLevelItemsAtSelectedLevel(this.home.getRooms(), levelOtherItems);
    addLevelItemsAtSelectedLevel(this.home.getDimensionLines(), levelOtherItems);
    addLevelItemsAtSelectedLevel(this.home.getLabels(), levelOtherItems);
    // First post to undo support that walls, rooms and dimension lines are deleted, 
    // otherwise data about joined walls and rooms index can't be stored       
    postDeleteItems(levelOtherItems, this.home.isBasePlanLocked(), this.home.isAllLevelsSelection());
    // Then delete items from plan
    doDeleteItems(levelOtherItems);

    this.home.deleteLevel(oldSelectedLevel);
    List levels = this.home.getLevels();
    final Level remainingLevel;
    final Float remainingLevelElevation;
    final boolean remainingLevelViewable;
    if (levels.size() == 1) {
      remainingLevel = levels.get(0);
      remainingLevelElevation = remainingLevel.getElevation();
      remainingLevelViewable = remainingLevel.isViewable();
      remainingLevel.setElevation(0);
      remainingLevel.setViewable(true);
    } else {
      remainingLevel = null;
      remainingLevelElevation = null;
      remainingLevelViewable = false;
    }    
    undoSupport.postEdit(new AbstractUndoableEdit() {
        @Override
        public void undo() throws CannotUndoException {
          super.undo();
          if (remainingLevel != null) {
            remainingLevel.setElevation(remainingLevelElevation);
            remainingLevel.setViewable(remainingLevelViewable);
          }
          home.addLevel(oldSelectedLevel);
          setSelectedLevel(oldSelectedLevel);
        }
        
        @Override
        public void redo() throws CannotRedoException {
          super.redo();
          home.deleteLevel(oldSelectedLevel);
          if (remainingLevel != null) {
            remainingLevel.setElevation(0);
            remainingLevel.setViewable(true);
          }
        }

        @Override
        public String getPresentationName() {
          return preferences.getLocalizedString(PlanController.class, "undoDeleteSelectedLevel");
        }      
      });
   
    // End compound edit
    undoSupport.endUpdate();
  }

  private void addLevelItemsAtSelectedLevel(Collection items, 
                                            List levelItems) {
    Level selectedLevel = this.home.getSelectedLevel();
    for (Selectable item : items) {
      if (item instanceof Elevatable
          && ((Elevatable)item).getLevel() == selectedLevel) {
        levelItems.add(item);
      }
    }
  }
  
  /**
   * Returns a new wall instance between (xStart,
   * yStart) and (xEnd, yEnd)
   * end points. The new wall is added to home and its start point is joined 
   * to the start of wallStartAtStart or 
   * the end of wallEndAtStart.
   */
  protected Wall createWall(float xStart, float yStart,
                            float xEnd, float yEnd,
                            Wall wallStartAtStart,
                            Wall wallEndAtStart) {
    // Create a new wall
    Wall newWall = new Wall(xStart, yStart, xEnd, yEnd, 
        this.preferences.getNewWallThickness(),
        this.preferences.getNewWallHeight(),
        this.preferences.getNewWallPattern());
    this.home.addWall(newWall);
    if (wallStartAtStart != null) {
      newWall.setWallAtStart(wallStartAtStart);
      wallStartAtStart.setWallAtStart(newWall);
    } else if (wallEndAtStart != null) {
      newWall.setWallAtStart(wallEndAtStart);
      wallEndAtStart.setWallAtEnd(newWall);
    }        
    return newWall;
  }
  
  /**
   * Joins the end point of wall to the start of
   * wallStartAtEnd or the end of wallEndAtEnd.
   */
  private void joinNewWallEndToWall(Wall wall, 
                                    Wall wallStartAtEnd, Wall wallEndAtEnd) {
    if (wallStartAtEnd != null) {
      wall.setWallAtEnd(wallStartAtEnd);
      wallStartAtEnd.setWallAtStart(wall);
      // Make wall end at the exact same position as wallAtEnd start point
      wall.setXEnd(wallStartAtEnd.getXStart());
      wall.setYEnd(wallStartAtEnd.getYStart());
    } else if (wallEndAtEnd != null) {
      wall.setWallAtEnd(wallEndAtEnd);
      wallEndAtEnd.setWallAtEnd(wall);
      // Make wall end at the exact same position as wallAtEnd end point
      wall.setXEnd(wallEndAtEnd.getXEnd());
      wall.setYEnd(wallEndAtEnd.getYEnd());
    }
  }
  
  /**
   * Returns the wall at (x, y) point,  
   * which has a start point not joined to any wall. 
   */
  private Wall getWallStartAt(float x, float y, Wall ignoredWall) {
    float margin = WALL_ENDS_PIXEL_MARGIN / getScale();
    for (Wall wall : this.home.getWalls()) {
      if (wall != ignoredWall
          && isLevelNullOrViewable(wall.getLevel())
          && wall.isAtLevel(this.home.getSelectedLevel())
          && wall.getWallAtStart() == null
          && wall.containsWallStartAt(x, y, margin)) 
        return wall;
    }
    return null;
  }

  /**
   * Returns the wall at (x, y) point,  
   * which has a end point not joined to any wall. 
   */
  private Wall getWallEndAt(float x, float y, Wall ignoredWall) {
    float margin = WALL_ENDS_PIXEL_MARGIN / getScale();
    for (Wall wall : this.home.getWalls()) {
      if (wall != ignoredWall
          && isLevelNullOrViewable(wall.getLevel())
          && wall.isAtLevel(this.home.getSelectedLevel())
          && wall.getWallAtEnd() == null
          && wall.containsWallEndAt(x, y, margin)) 
        return wall;
    }
    return null;
  }

  /**
   * Returns the selected wall with a start point 
   * at (x, y).
   */
  private Wall getResizedWallStartAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Wall
        && isItemResizable(selectedItems.get(0))) {
      Wall wall = (Wall)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (wall.isAtLevel(this.home.getSelectedLevel())
          && wall.containsWallStartAt(x, y, margin)) {
        return wall;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected wall with an end point at (x, y).
   */
  private Wall getResizedWallEndAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Wall
        && isItemResizable(selectedItems.get(0))) {
      Wall wall = (Wall)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (wall.isAtLevel(this.home.getSelectedLevel())
          && wall.containsWallEndAt(x, y, margin)) {
        return wall;
      }
    } 
    return null;
  }
  
  /**
   * Returns a new room instance with the given points. 
   * The new room is added to home.
   */
  protected Room createRoom(float [][] roomPoints) {
    Room newRoom = new Room(roomPoints);
    this.home.addRoom(newRoom);
    return newRoom;
  }
  
  /**
   * Returns the selected room with a point at (x, y).
   */
  private Room getResizedRoomAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Room
        && isItemResizable(selectedItems.get(0))) {
      Room room = (Room)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (room.isAtLevel(this.home.getSelectedLevel())
          && room.getPointIndexAt(x, y, margin) != -1) {
        return room;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected room with its name center point at (x, y).
   */
  private Room getRoomNameAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Room
        && isItemMovable(selectedItems.get(0))) {
      Room room = (Room)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (room.isAtLevel(this.home.getSelectedLevel())
          && room.getName() != null
          && room.getName().trim().length() > 0
          && room.isNameCenterPointAt(x, y, margin)) {
        return room;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected room with its 
   * name angle point at (x, y).
   */
  private Room getRoomRotatedNameAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Room
        && isItemMovable(selectedItems.get(0))) {
      Room room = (Room)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (room.isAtLevel(this.home.getSelectedLevel())
          && room.getName() != null
          && room.getName().trim().length() > 0
          && isTextAnglePointAt(room, room.getName(), room.getNameStyle(),
              room.getXCenter() + room.getNameXOffset(), room.getYCenter() + room.getNameYOffset(),
              room.getNameAngle(), x, y, margin)) {
        return room;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected room with its area center point at (x, y).
   */
  private Room getRoomAreaAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Room
        && isItemMovable(selectedItems.get(0))) {
      Room room = (Room)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (room.isAtLevel(this.home.getSelectedLevel())
          && room.isAreaVisible() 
          && room.isAreaCenterPointAt(x, y, margin)) {
        return room;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected room with its 
   * area angle point at (x, y).
   */
  private Room getRoomRotatedAreaAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Room
        && isItemMovable(selectedItems.get(0))) {
      Room room = (Room)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (room.isAtLevel(this.home.getSelectedLevel())
          && room.isAreaVisible()) {
        float area = room.getArea();
        if (area > 0.01f) {
          String areaText = this.preferences.getLengthUnit().getAreaFormatWithUnit().format(area);
          if (isTextAnglePointAt(room, areaText, room.getAreaStyle(),
                  room.getXCenter() + room.getAreaXOffset(), room.getYCenter() + room.getAreaYOffset(),
                  room.getAreaAngle(), x, y, margin)) {
            return room;        
          }
        }
      }
    } 
    return null;
  }
  
  /**
   * Adds a point to the selected room at the given coordinates and posts an undoable operation.
   * @since 5.0
   */
  public void addPointToSelectedRoom(final float x, final float y) {
    final List oldSelectedItems = this.home.getSelectedItems();
    if (oldSelectedItems.size() == 1
        && oldSelectedItems.get(0) instanceof Room
        && isItemResizable(oldSelectedItems.get(0))) {
      final Room room = (Room)oldSelectedItems.get(0);
      final float [][] points = room.getPoints();
      // Search the segment closest to (x, y)
      int closestSegmentIndex = -1;
      double smallestDistance = Double.MAX_VALUE;
      for (int i = 0; i < points.length; i++) {
        float [] point = points [i];
        float [] nextPoint = points [(i + 1) % points.length];
        double distanceToSegment = Line2D.ptSegDistSq(point [0], point [1], nextPoint [0], nextPoint [1], x, y);
        if (smallestDistance > distanceToSegment) {
          smallestDistance = distanceToSegment;
          closestSegmentIndex = i;
        }
      }    
      final int index = closestSegmentIndex + 1;
      room.addPoint(x, y, index);
      this.home.setSelectedItems(Arrays.asList(new Room [] {room}));
      // Upright an undoable edit
      UndoableEdit undoableEdit = new AbstractUndoableEdit() {      
          @Override
          public void undo() throws CannotUndoException {
            super.undo();
            room.removePoint(index);
            selectAndShowItems(oldSelectedItems);
          }
          
          @Override
          public void redo() throws CannotRedoException {
            super.redo();
            room.addPoint(x, y, index);
            selectAndShowItems(Arrays.asList(new Room [] {room}));
          }      
    
          @Override
          public String getPresentationName() {
            return preferences.getLocalizedString(PlanController.class, "undoAddRoomPointName");
          }      
        };
      this.undoSupport.postEdit(undoableEdit);
    }
  }
  
  /**
   * Returns true if the given point can be removed from the room.
   */
  public boolean isRoomPointDeletableAt(Room room, float x, float y) {
    return isItemResizable(room) 
        && room.getPointIndexAt(x, y, INDICATOR_PIXEL_MARGIN / getScale()) >= 0;
  }
  
  /**
   * Deletes the point of the selected room at the given coordinates and posts an undoable operation.
   * @since 5.0
   */
  public void deletePointFromSelectedRoom(float x, float y) {
    final List oldSelectedItems = this.home.getSelectedItems();
    if (oldSelectedItems.size() == 1
        && oldSelectedItems.get(0) instanceof Room
        && isItemResizable(oldSelectedItems.get(0))) {
      final Room room = (Room)oldSelectedItems.get(0);
      final int index = room.getPointIndexAt(x, y, INDICATOR_PIXEL_MARGIN / getScale());
      if (index >= 0) {
        float [][] points = room.getPoints();
        float [] point = points [index];
        final float xPoint = point [0];
        final float yPoint = point [1];
        
        room.removePoint(index);
        this.home.setSelectedItems(Arrays.asList(new Room [] {room}));
        // Upright an undoable edit
        UndoableEdit undoableEdit = new AbstractUndoableEdit() {      
            @Override
            public void undo() throws CannotUndoException {
              super.undo();
              room.addPoint(xPoint, yPoint, index);
              selectAndShowItems(oldSelectedItems);
            }
            
            @Override
            public void redo() throws CannotRedoException {
              super.redo();
              room.removePoint(index);
              selectAndShowItems(Arrays.asList(new Room [] {room}));
            }      
      
            @Override
            public String getPresentationName() {
              return preferences.getLocalizedString(PlanController.class, "undoDeleteRoomPointName");
            }      
          };
        this.undoSupport.postEdit(undoableEdit);
      }
    }
  }

  /**
   * Returns a new dimension instance joining (xStart,
   * yStart) and (xEnd, yEnd) points. 
   * The new dimension line is added to home.
   */
  protected DimensionLine createDimensionLine(float xStart, float yStart, 
                                              float xEnd, float yEnd, 
                                              float offset) {
    DimensionLine newDimensionLine = new DimensionLine(xStart, yStart, xEnd, yEnd, offset);
    this.home.addDimensionLine(newDimensionLine);
    return newDimensionLine;
  }

  /**
   * Returns the selected dimension line with an end extension line
   * at (x, y).
   */
  private DimensionLine getResizedDimensionLineStartAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof DimensionLine
        && isItemResizable(selectedItems.get(0))) {
      DimensionLine dimensionLine = (DimensionLine)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (dimensionLine.isAtLevel(this.home.getSelectedLevel())
          && dimensionLine.containsStartExtensionLinetAt(x, y, margin)) {
        return dimensionLine;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected dimension line with an end extension line
   * at (x, y).
   */
  private DimensionLine getResizedDimensionLineEndAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof DimensionLine
        && isItemResizable(selectedItems.get(0))) {
      DimensionLine dimensionLine = (DimensionLine)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (dimensionLine.isAtLevel(this.home.getSelectedLevel())
          && dimensionLine.containsEndExtensionLineAt(x, y, margin)) {
        return dimensionLine;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected dimension line with a point
   * at (x, y) at its middle.
   */
  private DimensionLine getOffsetDimensionLineAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof DimensionLine
        && isItemResizable(selectedItems.get(0))) {
      DimensionLine dimensionLine = (DimensionLine)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (dimensionLine.isAtLevel(this.home.getSelectedLevel())
          && dimensionLine.isMiddlePointAt(x, y, margin)) {
        return dimensionLine;
      }
    } 
    return null;
  }
  
  /**
   * Returns a new polyline instance with the given points. 
   * The new polyline is added to home.
   */
  private Polyline createPolyline(float [][] polylinePoints) {
    Polyline newPolyline = new Polyline(polylinePoints);
    LengthUnit lengthUnit = preferences.getLengthUnit();
    newPolyline.setThickness(lengthUnit == LengthUnit.INCH || lengthUnit == LengthUnit.INCH_DECIMALS 
        ? LengthUnit.inchToCentimeter(1) 
        : 2);
    this.home.addPolyline(newPolyline);
    return newPolyline;
  }

  /**
   * Returns the selected polyline with a point at (x, y).
   */
  private Polyline getResizedPolylineAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Polyline
        && isItemResizable(selectedItems.get(0))) {
      Polyline polyline = (Polyline)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (polyline.isAtLevel(this.home.getSelectedLevel())
          && polyline.getPointIndexAt(x, y, margin) != -1) {
        return polyline;
      }
    } 
    return null;
  }

  /**
   * Returns the selected item at (x, y) point.
   */
  private boolean isItemSelectedAt(float x, float y) {
    float margin = PIXEL_MARGIN / getScale();
    for (Selectable item : this.home.getSelectedItems()) {
      if (item.containsPoint(x, y, margin)) {
        return true;
      }
    }
    return false;
  }
  
  /**
   * Returns the selectable item at (x, y) point.
   */
  public Selectable getSelectableItemAt(float x, float y) {
    return getSelectableItemAt(x, y, true);
  }
  
  /**
   * Returns the selectable item at (x, y) point.
   */
  private Selectable getSelectableItemAt(float x, float y, boolean ignoreGroupsFurniture) {
    List selectableItems = getSelectableItemsAt(x, y, true, ignoreGroupsFurniture);
    if (selectableItems.size() != 0) {
      return selectableItems.get(0);
    } else {
      return null;
    }
  }
  
  /**
   * Returns the selectable items at (x, y) point.
   */
  public List getSelectableItemsAt(float x, float y) {
    return getSelectableItemsAt(x, y, false, true);
  }
  
  /**
   * Returns the selectable items at (x, y) point.
   */
  private List getSelectableItemsAt(float x, float y, 
                                                boolean stopAtFirstItem,
                                                boolean ignoreGroupsFurniture) {
    List items = new ArrayList();
    float margin = PIXEL_MARGIN / getScale();
    float textMargin = PIXEL_MARGIN / 2 / getScale();
    ObserverCamera camera = this.home.getObserverCamera();
    if (camera != null
        && camera == this.home.getCamera()
        && camera.containsPoint(x, y, margin)) {
      items.add(camera);
      if (stopAtFirstItem) {
        return items;
      }
    }
    
    boolean basePlanLocked = this.home.isBasePlanLocked();
    Level selectedLevel = this.home.getSelectedLevel();
    for (Label label : this.home.getLabels()) {
      if ((!basePlanLocked 
            || !isItemPartOfBasePlan(label)) 
          && isLevelNullOrViewable(label.getLevel())
          && label.isAtLevel(selectedLevel)
          && (label.containsPoint(x, y, margin)
              || isItemTextAt(label, label.getText(), label.getStyle(), 
                    label.getX(), label.getY(), label.getAngle(), x, y, textMargin))) {
        items.add(label);
        if (stopAtFirstItem) {
          return items;
        }
      }
    }    
    
    for (DimensionLine dimensionLine : this.home.getDimensionLines()) {
      if ((!basePlanLocked 
            || !isItemPartOfBasePlan(dimensionLine))
          && isLevelNullOrViewable(dimensionLine.getLevel())
          && dimensionLine.isAtLevel(selectedLevel)
          && dimensionLine.containsPoint(x, y, margin)) {
        items.add(dimensionLine);
        if (stopAtFirstItem) {
          return items;
        }
      }
    }    
    
    List polylines = this.home.getPolylines();
    // Search in home polylines in reverse order to give priority to last drawn polyline
    for (int i = polylines.size() - 1; i >= 0; i--) {
      Polyline polyline = polylines.get(i);
      if ((!basePlanLocked 
            || !isItemPartOfBasePlan(polyline))
          && isLevelNullOrViewable(polyline.getLevel())
          && polyline.isAtLevel(selectedLevel)
          && polyline.containsPoint(x, y, margin)) {
        items.add(polyline);
        if (stopAtFirstItem) {
          return items;
        }
      }
    }    
    
    List furniture = this.home.getFurniture();
    // Search in home furniture in reverse order to give priority to last drawn piece
    // at highest elevation in case it covers an other piece
    List foundFurniture = new ArrayList();
    HomePieceOfFurniture foundPiece = null;
    for (int i = furniture.size() - 1; i >= 0; i--) {
      HomePieceOfFurniture piece = furniture.get(i);
      if ((!basePlanLocked 
            || !isItemPartOfBasePlan(piece))
          && isPieceOfFurnitureVisibleAtSelectedLevel(piece)) {
        if (piece.containsPoint(x, y, margin)) {
          foundFurniture.add(piece);
          if (foundPiece == null
              || piece.getGroundElevation() > foundPiece.getGroundElevation()) {
            foundPiece = piece;
          }
        } else if (foundPiece == null) { 
          // Search if piece name contains point in case it is drawn outside of the piece
          String pieceName = piece.getName();
          if (pieceName != null
              && piece.isNameVisible() 
              && isItemTextAt(piece, pieceName, piece.getNameStyle(), 
                  piece.getX() + piece.getNameXOffset(), 
                  piece.getY() + piece.getNameYOffset(), piece.getNameAngle(), x, y, textMargin)) {
            foundFurniture.add(piece);
            foundPiece = piece;
          }
        }
      }
    }
    if (foundPiece == null
        && basePlanLocked) {
      // Check among the furniture that is already selected if there's a movable piece at the given location
      for (Selectable item : home.getSelectedItems()) {
        if (item instanceof HomePieceOfFurniture) {
          HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
          if (!isItemPartOfBasePlan(piece)
              && isPieceOfFurnitureVisibleAtSelectedLevel(piece) 
              && (piece.containsPoint(x, y, margin)
                  || piece.getName() != null
                      && piece.isNameVisible() 
                      && isItemTextAt(piece, piece.getName(), piece.getNameStyle(), 
                          piece.getX() + piece.getNameXOffset(), 
                          piece.getY() + piece.getNameYOffset(), piece.getNameAngle(), x, y, textMargin))) {
            foundFurniture.add(piece);
            foundPiece = piece;
            if (stopAtFirstItem) {
              break;
            }
          }
        }
      }
    }
    if (foundPiece != null
        && stopAtFirstItem) {
      if (!ignoreGroupsFurniture 
          && (foundPiece instanceof HomeFurnitureGroup)) {
        List selectedItems = this.home.getSelectedItems();
        if (selectedItems.size() >= 1) {
          // If selected items are in the same group 
          if ((selectedItems.size() == 1 && selectedItems.get(0) == foundPiece)
              || ((HomeFurnitureGroup)foundPiece).getAllFurniture().containsAll(selectedItems)) {
            for (Selectable selectedItem : selectedItems) {
              if (selectedItem instanceof HomeFurnitureGroup) {
                // Search the piece at point among the furniture of the selected group 
                List groupFurniture = ((HomeFurnitureGroup)selectedItem).getFurniture();
                for (int i = groupFurniture.size() - 1; i >= 0; i--) {
                  HomePieceOfFurniture piece = groupFurniture.get(i);
                  if (!selectedItems.contains(piece) 
                      && piece.containsPoint(x, y, margin)) {
                    return Arrays.asList(new Selectable [] {piece});
                  }
                }
              }
            }
            // Search the piece at point among the groups of selected furniture
            for (Selectable selectedItem : selectedItems) {
              if (selectedItem instanceof HomePieceOfFurniture) {
                List groupFurniture = getFurnitureInSameGroup((HomePieceOfFurniture)selectedItem);
                for (int i = groupFurniture.size() - 1; i >= 0; i--) {
                  HomePieceOfFurniture piece = groupFurniture.get(i);
                  if (piece.containsPoint(x, y, margin)) {
                    return Arrays.asList(new Selectable [] {piece});
                  }
                }
              }
            }
          }
        }
      }
      return Arrays.asList(new Selectable [] {foundPiece});
    } else {
      Collections.sort(foundFurniture, new Comparator() {
          public int compare(HomePieceOfFurniture p1, HomePieceOfFurniture p2) {
            return -Float.compare(p1.getGroundElevation(), p2.getGroundElevation());
          }
        });
      items.addAll(foundFurniture);
      for (Wall wall : this.home.getWalls()) {
        if ((!basePlanLocked 
              || !isItemPartOfBasePlan(wall))
            && isLevelNullOrViewable(wall.getLevel())
            && wall.isAtLevel(selectedLevel)
            && wall.containsPoint(x, y, margin)) {
          items.add(wall);
          if (stopAtFirstItem) {
            return items;
          }
        }
      }    

      List rooms = this.home.getRooms();
      // Search in home rooms in reverse order to give priority to last drawn room
      // at highest elevation in case it covers an other piece
      Room foundRoom = null;
      for (int i = rooms.size() - 1; i >= 0; i--) {
        Room room = rooms.get(i);
        if ((!basePlanLocked 
              || !isItemPartOfBasePlan(room)) 
            && isLevelNullOrViewable(room.getLevel())
            && room.isAtLevel(selectedLevel)) {
          if (room.containsPoint(x, y, margin)) {
            items.add(room);
             if (foundRoom == null
                 || room.isCeilingVisible() && !foundRoom.isCeilingVisible()) {
               foundRoom = room;
             }
          } else { 
            // Search if room name contains point in case it is drawn outside of the room
            String roomName = room.getName();
            if (roomName != null 
                && isItemTextAt(room, roomName, room.getNameStyle(), 
                  room.getXCenter() + room.getNameXOffset(), 
                  room.getYCenter() + room.getNameYOffset(), room.getNameAngle(), x, y, textMargin)) {
              items.add(room);
              foundRoom = room;
            }
            // Search if room area contains point in case its text is drawn outside of the room 
            if (room.isAreaVisible()) {
              String areaText = this.preferences.getLengthUnit().getAreaFormatWithUnit().format(room.getArea());
              if (isItemTextAt(room, areaText, room.getAreaStyle(), 
                  room.getXCenter() + room.getAreaXOffset(), 
                  room.getYCenter() + room.getAreaYOffset(), room.getAreaAngle(), x, y, textMargin)) {
                items.add(room);
                foundRoom = room;
              }
            }
          }
        }
      }
      if (foundRoom != null
          && stopAtFirstItem) {
        return Arrays.asList(new Selectable [] {foundRoom});
      } else {
        Compass compass = this.home.getCompass();
        if ((!basePlanLocked 
              || !isItemPartOfBasePlan(compass))
            && compass.containsPoint(x, y, textMargin)) {
          items.add(compass);
        }
        return items;
      }
    }
  }

  /**
   * Returns true if the text of an item displayed
   * at the point (xText, yText) contains the point (x, y).
   */
  private boolean isItemTextAt(Selectable item, String text, TextStyle textStyle, float xText, float yText, float textAngle, 
                               float x, float y, float textMargin) {
    if (textStyle == null) {
      textStyle = this.preferences.getDefaultTextStyle(item.getClass());              
    }          
    float [][] textBounds = getView().getTextBounds(text, textStyle, xText, yText, textAngle);
    return getPath(textBounds).intersects(x - textMargin, y - textMargin, 2 * textMargin, 2 * textMargin);
  }
  
  /**
   * Returns the items that intersects with the rectangle of (x0,
   * y0), (x1, y1) opposite corners.
   */
  protected List getSelectableItemsIntersectingRectangle(float x0, float y0, float x1, float y1) {
    List items = new ArrayList();
    boolean basePlanLocked = this.home.isBasePlanLocked();
    for (Selectable item : getVisibleItemsAtSelectedLevel()) {
      if ((!basePlanLocked 
            || !isItemPartOfBasePlan(item))
          && item.intersectsRectangle(x0, y0, x1, y1)) {
        items.add(item);
      }
    }
    ObserverCamera camera = this.home.getObserverCamera();
    if (camera != null && camera.intersectsRectangle(x0, y0, x1, y1)) {
      items.add(camera);
    }
    return items;
  }

  /**
   * Returns the selected piece of furniture with a point 
   * at (x, y) that can be used to rotate the piece.
   */
  private HomePieceOfFurniture getRotatedPieceOfFurnitureAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof HomePieceOfFurniture
        && isItemMovable(selectedItems.get(0))) {
      HomePieceOfFurniture piece = (HomePieceOfFurniture)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (isPieceOfFurnitureVisibleAtSelectedLevel(piece)
          && piece.isTopLeftPointAt(x, y, margin)
          // Ignore piece shape to ensure there's always enough space to drag it
          && !piece.containsPoint(x, y, 0)) {
        return piece;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected piece of furniture with a point 
   * at (x, y) that can be used to elevate the piece.
   */
  private HomePieceOfFurniture getElevatedPieceOfFurnitureAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof HomePieceOfFurniture
        && isItemMovable(selectedItems.get(0))) {
      HomePieceOfFurniture piece = (HomePieceOfFurniture)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (isPieceOfFurnitureVisibleAtSelectedLevel(piece)
          && piece.isTopRightPointAt(x, y, margin)
          // Ignore piece shape to ensure there's always enough space to drag it
          && !piece.containsPoint(x, y, 0)) {
        return piece;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected piece of furniture with a point 
   * at (x, y) that can be used to resize the height 
   * of the piece.
   */
  private HomePieceOfFurniture getHeightResizedPieceOfFurnitureAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof HomePieceOfFurniture) {
      HomePieceOfFurniture piece = (HomePieceOfFurniture)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (isPieceOfFurnitureVisibleAtSelectedLevel(piece)
          && piece.isResizable()
          && isItemResizable(piece) 
          && piece.isBottomLeftPointAt(x, y, margin)
          // Ignore piece shape to ensure there's always enough space to drag it
          && !piece.containsPoint(x, y, 0)) {
        return piece;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected piece of furniture with a point 
   * at (x, y) that can be used to resize 
   * the width and the depth of the piece.
   */
  private HomePieceOfFurniture getWidthAndDepthResizedPieceOfFurnitureAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof HomePieceOfFurniture
        && isItemResizable(selectedItems.get(0))) {
      HomePieceOfFurniture piece = (HomePieceOfFurniture)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (isPieceOfFurnitureVisibleAtSelectedLevel(piece)
          && piece.isResizable()
          && isItemResizable(piece) 
          && piece.isBottomRightPointAt(x, y, margin)
          // Ignore piece shape to ensure there's always enough space to drag it
          && !piece.containsPoint(x, y, 0)) {
        return piece;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected light with a point at (x, y) 
   * that can be used to resize the power of the light.
   */
  private HomeLight getModifiedLightPowerAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof HomeLight) {
      HomeLight light = (HomeLight)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN * (1 / getScale());
      if (isPieceOfFurnitureVisibleAtSelectedLevel(light)
          && light.isBottomLeftPointAt(x, y, margin)
          // Ignore piece shape to ensure there's always enough space to drag it
          && !light.containsPoint(x, y, 0)) {
        return light;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected piece of furniture with its 
   * name center point at (x, y).
   */
  private HomePieceOfFurniture getPieceOfFurnitureNameAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof HomePieceOfFurniture
        && isItemMovable(selectedItems.get(0))) {
      HomePieceOfFurniture piece = (HomePieceOfFurniture)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (isPieceOfFurnitureVisibleAtSelectedLevel(piece)
          && piece.isNameVisible()
          && piece.getName().trim().length() > 0
          && piece.isNameCenterPointAt(x, y, margin)) {
        return piece;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected piece of furniture with its 
   * name angle point at (x, y).
   */
  private HomePieceOfFurniture getPieceOfFurnitureRotatedNameAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof HomePieceOfFurniture
        && isItemMovable(selectedItems.get(0))) {
      HomePieceOfFurniture piece = (HomePieceOfFurniture)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (isPieceOfFurnitureVisibleAtSelectedLevel(piece)
          && piece.isNameVisible()
          && piece.getName().trim().length() > 0
          && isTextAnglePointAt(piece, piece.getName(), piece.getNameStyle(),
                piece.getX() + piece.getNameXOffset(), piece.getY() + piece.getNameYOffset(),
                piece.getNameAngle(), x, y, margin)) {
        return piece;
      }
    } 
    return null;
  }
  
  /**
   * Returns true if the angle indicator of the text of an item displayed
   * at the point (xText, yText) is equal to the point (x, y).
   */
  private boolean isTextAnglePointAt(Selectable item, String text, TextStyle textStyle, float xText, float yText, float textAngle, 
                                     float x, float y, float margin) {
    if (textStyle == null) {
      textStyle = this.preferences.getDefaultTextStyle(item.getClass());              
    }          
    float [][] textBounds = getView().getTextBounds(text, textStyle, xText, yText, textAngle);
    return Math.abs(x - (textBounds [0][0] + textBounds [1][0]) / 2) <= margin 
        && Math.abs(y - (textBounds [0][1] + textBounds [1][1]) / 2) <= margin; 
  }

  /**
   * Returns the selected label with its angle point at (x, y).
   */
  private Label getRotatedLabelAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Label
        && isItemMovable(selectedItems.get(0))) {
      Label label = (Label)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      if (label.isAtLevel(this.home.getSelectedLevel())
          && isTextAnglePointAt(label, label.getText(), label.getStyle(),
                label.getX(), label.getY(), label.getAngle(), x, y, margin)) {
        return label;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected label with its elevation point at (x, y).
   */
  private Label getElevatedLabelAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Label) {
      Label label = (Label)selectedItems.get(0);
      if (label.getPitch() != null
          && isItemMovable(label)) {
        float margin = INDICATOR_PIXEL_MARGIN / getScale();
        if (label.isAtLevel(this.home.getSelectedLevel())) {
          float [][] textBounds = getView().getTextBounds(label.getText(), getItemTextStyle(label, label.getStyle()), 
              label.getX(), label.getY(), label.getAngle());
          if (Math.abs(x - (textBounds [2][0] + textBounds [3][0]) / 2) <= margin 
              && Math.abs(y - (textBounds [2][1] + textBounds [3][1]) / 2) <= margin) {
            return label;
          }
        }
      } 
    }
    return null;
  }
  
  /**
   * Returns the selected camera with a point at (x, y) 
   * that can be used to change the camera yaw angle.
   */
  private Camera getYawRotatedCameraAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Camera
        && isItemResizable(selectedItems.get(0))) {
      ObserverCamera camera = (ObserverCamera)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      float [][] cameraPoints = camera.getPoints();
      // Check if (x,y) matches the point between the first and the last points 
      // of the rectangle surrounding camera
      float xMiddleFirstAndLastPoint = (cameraPoints [0][0] + cameraPoints [3][0]) / 2; 
      float yMiddleFirstAndLastPoint = (cameraPoints [0][1] + cameraPoints [3][1]) / 2;      
      if (Math.abs(x - xMiddleFirstAndLastPoint) <= margin 
          && Math.abs(y - yMiddleFirstAndLastPoint) <= margin
          // Ignore camera shape to ensure there's always enough space to drag it
          && !camera.containsPoint(x, y, 0)) {
        return camera;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected camera with a point at (x, y) 
   * that can be used to change the camera pitch angle.
   */
  private Camera getPitchRotatedCameraAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Camera
        && isItemResizable(selectedItems.get(0))) {
      ObserverCamera camera = (ObserverCamera)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      float [][] cameraPoints = camera.getPoints();
      // Check if (x,y) matches the point between the second and the third points
      // of the rectangle surrounding camera
      float xMiddleFirstAndLastPoint = (cameraPoints [1][0] + cameraPoints [2][0]) / 2; 
      float yMiddleFirstAndLastPoint = (cameraPoints [1][1] + cameraPoints [2][1]) / 2;      
      if (Math.abs(x - xMiddleFirstAndLastPoint) <= margin 
          && Math.abs(y - yMiddleFirstAndLastPoint) <= margin
          // Ignore camera shape to ensure there's always enough space to drag it
          && !camera.containsPoint(x, y, 0)) {
        return camera;
      }
    } 
    return null;
  }

  /**
   * Returns the selected camera with a point at (x, y) 
   * that can be used to change the camera elevation.
   */
  private Camera getElevatedCameraAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Camera
        && isItemResizable(selectedItems.get(0))) {
      ObserverCamera camera = (ObserverCamera)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      float [][] cameraPoints = camera.getPoints();
      // Check if (x,y) matches the point between the first and the second points 
      // of the rectangle surrounding camera
      float xMiddleFirstAndSecondPoint = (cameraPoints [0][0] + cameraPoints [1][0]) / 2; 
      float yMiddleFirstAndSecondPoint = (cameraPoints [0][1] + cameraPoints [1][1]) / 2;      
      if (Math.abs(x - xMiddleFirstAndSecondPoint) <= margin 
          && Math.abs(y - yMiddleFirstAndSecondPoint) <= margin
          // Ignore camera shape to ensure there's always enough space to drag it
          && !camera.containsPoint(x, y, 0)) {
        return camera;
      }
    } 
    return null;
  }

  /**
   * Returns the selected compass with a point 
   * at (x, y) that can be used to rotate it.
   */
  private Compass getRotatedCompassAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Compass
        && isItemMovable(selectedItems.get(0))) {
      Compass compass = (Compass)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      float [][] compassPoints = compass.getPoints();
      // Check if (x,y) matches the point between the third and the fourth points (South point) 
      // of the rectangle surrounding compass
      float xMiddleThirdAndFourthPoint = (compassPoints [2][0] + compassPoints [3][0]) / 2; 
      float yMiddleThirdAndFourthPoint = (compassPoints [2][1] + compassPoints [3][1]) / 2;      
      if (Math.abs(x - xMiddleThirdAndFourthPoint) <= margin 
          && Math.abs(y - yMiddleThirdAndFourthPoint) <= margin
          // Ignore camera shape to ensure there's always enough space to drag it
          && !compass.containsPoint(x, y, 0)) {
        return compass;
      }
    } 
    return null;
  }
  
  /**
   * Returns the selected compass with a point 
   * at (x, y) that can be used to resize it.
   */
  private Compass getResizedCompassAt(float x, float y) {
    List selectedItems = this.home.getSelectedItems();
    if (selectedItems.size() == 1
        && selectedItems.get(0) instanceof Compass
        && isItemMovable(selectedItems.get(0))) {
      Compass compass = (Compass)selectedItems.get(0);
      float margin = INDICATOR_PIXEL_MARGIN / getScale();
      float [][] compassPoints = compass.getPoints();
      // Check if (x,y) matches the point between the second and the third points (East point) 
      // of the rectangle surrounding compass
      float xMiddleSecondAndThirdPoint = (compassPoints [1][0] + compassPoints [2][0]) / 2; 
      float yMiddleSecondAndThirdPoint = (compassPoints [1][1] + compassPoints [2][1]) / 2;      
      if (Math.abs(x - xMiddleSecondAndThirdPoint) <= margin 
          && Math.abs(y - yMiddleSecondAndThirdPoint) <= margin
          // Ignore camera shape to ensure there's always enough space to drag it
          && !compass.containsPoint(x, y, 0)) {
        return compass;
      }
    } 
    return null;
  }
  
  /**
   * Deletes items in plan and record it as an undoable operation.
   */
  public void deleteItems(List items) {
    List deletedItems = new ArrayList(items.size());
    for (Selectable item : items) {
      if (isItemDeletable(item)) {
        deletedItems.add(item);
      }
    }
    
    if (!deletedItems.isEmpty()) {
      this.undoSupport.beginUpdate();
      // Remove selectionListener to avoid level selection change 
      // when selected items are deleted
      this.home.removeSelectionListener(this.selectionListener);
      // Start a compound edit that deletes walls, furniture and dimension lines from home
      
      final boolean allLevelsSelection = home.isAllLevelsSelection();
      final List selectedItems = new ArrayList(items);      
      // Add a undoable edit that will select the undeleted items at undo
      this.undoSupport.postEdit(new AbstractUndoableEdit() {      
          @Override
          public void undo() throws CannotRedoException {
            super.undo();
            selectAndShowItems(selectedItems, allLevelsSelection);
          }
          
          @Override
          public void redo() throws CannotRedoException {
            super.redo();
            home.removeSelectionListener(selectionListener);
          }
        });

      // Delete furniture with inherited method
      deleteFurniture(Home.getFurnitureSubList(deletedItems));      

      List deletedOtherItems = 
          new ArrayList(Home.getWallsSubList(deletedItems));
      deletedOtherItems.addAll(Home.getRoomsSubList(deletedItems));
      deletedOtherItems.addAll(Home.getDimensionLinesSubList(deletedItems));
      deletedOtherItems.addAll(Home.getPolylinesSubList(deletedItems));
      deletedOtherItems.addAll(Home.getLabelsSubList(deletedItems));
      // First post to undo support that walls, rooms and dimension lines are deleted, 
      // otherwise data about joined walls and rooms index can't be stored       
      postDeleteItems(deletedOtherItems, this.home.isBasePlanLocked(), this.home.isAllLevelsSelection());
      // Then delete items from plan
      doDeleteItems(deletedOtherItems);
      this.home.addSelectionListener(this.selectionListener);

      this.undoSupport.postEdit(new AbstractUndoableEdit() {      
          @Override
          public void redo() throws CannotRedoException {
            super.redo();
            home.addSelectionListener(selectionListener);
          }
        });
      
      // End compound edit
      this.undoSupport.endUpdate();
    }          
  }

  /**
   * Posts an undoable delete items operation about deletedItems.
   */
  private void postDeleteItems(final List deletedItems,
                               final boolean basePlanLocked, 
                               final boolean allLevelsSelection) {
    // Manage walls
    List deletedWalls = Home.getWallsSubList(deletedItems);
    // Get joined walls data for undo operation
    final JoinedWall [] joinedDeletedWalls = JoinedWall.getJoinedWalls(deletedWalls);

    // Manage rooms and their index
    List deletedRooms = Home.getRoomsSubList(deletedItems);
    List homeRooms = this.home.getRooms(); 
    // Sort the deleted rooms in the ascending order of their index in home
    Map sortedMap = new TreeMap(); 
    for (Room room : deletedRooms) {
      sortedMap.put(homeRooms.indexOf(room), room); 
    }
    final Room [] rooms = sortedMap.values().toArray(new Room [sortedMap.size()]); 
    final int [] roomsIndices = new int [rooms.length];
    final Level [] roomsLevels = new Level [rooms.length]; 
    int i = 0;
    for (int index : sortedMap.keySet()) {
      roomsIndices [i] = index;
      roomsLevels [i] = rooms [i].getLevel();
      i++;
    }
    
    // Manage dimension lines
    List deletedDimensionLines = Home.getDimensionLinesSubList(deletedItems);
    final DimensionLine [] dimensionLines = deletedDimensionLines.toArray(
        new DimensionLine [deletedDimensionLines.size()]);
    final Level [] dimensionLinesLevels = new Level [dimensionLines.length]; 
    for (i = 0; i < dimensionLines.length; i++) {
      dimensionLinesLevels [i] = dimensionLines [i].getLevel();
    }
    
    // Manage polylines and their index
    List deletedPolylines = Home.getPolylinesSubList(deletedItems);
    List homePolylines = this.home.getPolylines(); 
    // Sort the deleted polylines in the ascending order of their index in home
    Map sortedPolylinesMap = new TreeMap(); 
    for (Polyline polyline : deletedPolylines) {
      sortedPolylinesMap.put(homePolylines.indexOf(polyline), polyline); 
    }
    final Polyline [] polylines = sortedPolylinesMap.values().toArray(new Polyline [sortedPolylinesMap.size()]); 
    final int [] polylinesIndices = new int [polylines.length];
    final Level [] polylinesLevels = new Level [polylines.length]; 
    i = 0;
    for (int index : sortedPolylinesMap.keySet()) {
      polylinesIndices [i] = index; 
      polylinesLevels [i] = polylines [i].getLevel();
      i++;
    }

    // Manage labels
    List




© 2015 - 2024 Weber Informatics LLC | Privacy Policy