1 /*
2  * PlanController.java 2 juin 2006
3  *
4  * Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks <info@eteks.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 package com.eteks.sweethome3d.viewcontroller;
21 
22 import java.awt.Shape;
23 import java.awt.geom.AffineTransform;
24 import java.awt.geom.Area;
25 import java.awt.geom.GeneralPath;
26 import java.awt.geom.Line2D;
27 import java.awt.geom.PathIterator;
28 import java.awt.geom.Point2D;
29 import java.awt.geom.Rectangle2D;
30 import java.beans.PropertyChangeEvent;
31 import java.beans.PropertyChangeListener;
32 import java.beans.PropertyChangeSupport;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collection;
36 import java.util.Collections;
37 import java.util.Comparator;
38 import java.util.Hashtable;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.TreeMap;
42 
43 import javax.swing.undo.AbstractUndoableEdit;
44 import javax.swing.undo.CannotRedoException;
45 import javax.swing.undo.CannotUndoException;
46 import javax.swing.undo.UndoableEdit;
47 import javax.swing.undo.UndoableEditSupport;
48 
49 import com.eteks.sweethome3d.model.BackgroundImage;
50 import com.eteks.sweethome3d.model.Baseboard;
51 import com.eteks.sweethome3d.model.Camera;
52 import com.eteks.sweethome3d.model.CollectionEvent;
53 import com.eteks.sweethome3d.model.CollectionListener;
54 import com.eteks.sweethome3d.model.Compass;
55 import com.eteks.sweethome3d.model.DimensionLine;
56 import com.eteks.sweethome3d.model.Elevatable;
57 import com.eteks.sweethome3d.model.Home;
58 import com.eteks.sweethome3d.model.HomeDoorOrWindow;
59 import com.eteks.sweethome3d.model.HomeFurnitureGroup;
60 import com.eteks.sweethome3d.model.HomeLight;
61 import com.eteks.sweethome3d.model.HomePieceOfFurniture;
62 import com.eteks.sweethome3d.model.HomeTexture;
63 import com.eteks.sweethome3d.model.Label;
64 import com.eteks.sweethome3d.model.LengthUnit;
65 import com.eteks.sweethome3d.model.Level;
66 import com.eteks.sweethome3d.model.ObserverCamera;
67 import com.eteks.sweethome3d.model.Polyline;
68 import com.eteks.sweethome3d.model.Room;
69 import com.eteks.sweethome3d.model.Selectable;
70 import com.eteks.sweethome3d.model.SelectionEvent;
71 import com.eteks.sweethome3d.model.SelectionListener;
72 import com.eteks.sweethome3d.model.TextStyle;
73 import com.eteks.sweethome3d.model.UserPreferences;
74 import com.eteks.sweethome3d.model.Wall;
75 
76 /**
77  * A MVC controller for the plan view.
78  * @author Emmanuel Puybaret
79  */
80 public class PlanController extends FurnitureController implements Controller {
81   public enum Property {MODE, MODIFICATION_STATE, BASE_PLAN_MODIFICATION_STATE, SCALE}
82 
83   /**
84    * Selectable modes in controller.
85    */
86   public static class Mode {
87     // Don't qualify Mode as an enumeration to be able to extend Mode class
88     public static final Mode SELECTION               = new Mode("SELECTION");
89     public static final Mode PANNING                 = new Mode("PANNING");
90     public static final Mode WALL_CREATION           = new Mode("WALL_CREATION");
91     public static final Mode ROOM_CREATION           = new Mode("ROOM_CREATION");
92     public static final Mode POLYLINE_CREATION       = new Mode("POLYLINE_CREATION");
93     public static final Mode DIMENSION_LINE_CREATION = new Mode("DIMENSION_LINE_CREATION");
94     public static final Mode LABEL_CREATION          = new Mode("LABEL_CREATION");
95 
96     private final String name;
97 
Mode(String name)98     protected Mode(String name) {
99       this.name = name;
100     }
101 
name()102     public final String name() {
103       return this.name;
104     }
105 
106     @Override
toString()107     public String toString() {
108       return this.name;
109     }
110   };
111 
112   /**
113    * Fields that can be edited in plan view.
114    */
115   public static enum EditableProperty {X, Y, LENGTH, ANGLE, THICKNESS, OFFSET, ARC_EXTENT}
116 
117   private static final String SCALE_VISUAL_PROPERTY = "com.eteks.sweethome3d.SweetHome3D.PlanScale";
118 
119   private static final int PIXEL_MARGIN           = 4;
120   private static final int INDICATOR_PIXEL_MARGIN = 5;
121   private static final int WALL_ENDS_PIXEL_MARGIN = 2;
122 
123   private final Home                  home;
124   private final UserPreferences       preferences;
125   private final ViewFactory           viewFactory;
126   private final ContentManager        contentManager;
127   private final UndoableEditSupport   undoSupport;
128   private final PropertyChangeSupport propertyChangeSupport;
129   private PlanView                    planView;
130   private SelectionListener           selectionListener;
131   private PropertyChangeListener      wallChangeListener;
132   private PropertyChangeListener      furnitureSizeChangeListener;
133   // Possibles states
134   private final ControllerState       selectionState;
135   private final ControllerState       rectangleSelectionState;
136   private final ControllerState       selectionMoveState;
137   private final ControllerState       panningState;
138   private final ControllerState       dragAndDropState;
139   private final ControllerState       wallCreationState;
140   private final ControllerState       wallDrawingState;
141   private final ControllerState       wallResizeState;
142   private final ControllerState       wallArcExtentState;
143   private final ControllerState       pieceOfFurnitureRotationState;
144   private final ControllerState       pieceOfFurniturePitchRotationState;
145   private final ControllerState       pieceOfFurnitureRollRotationState;
146   private final ControllerState       pieceOfFurnitureElevationState;
147   private final ControllerState       pieceOfFurnitureHeightState;
148   private final ControllerState       pieceOfFurnitureResizeState;
149   private final ControllerState       lightPowerModificationState;
150   private final ControllerState       pieceOfFurnitureNameOffsetState;
151   private final ControllerState       pieceOfFurnitureNameRotationState;
152   private final ControllerState       cameraYawRotationState;
153   private final ControllerState       cameraPitchRotationState;
154   private final ControllerState       cameraElevationState;
155   private final ControllerState       dimensionLineCreationState;
156   private final ControllerState       dimensionLineDrawingState;
157   private final ControllerState       dimensionLineResizeState;
158   private final ControllerState       dimensionLineOffsetState;
159   private final ControllerState       roomCreationState;
160   private final ControllerState       roomDrawingState;
161   private final ControllerState       roomResizeState;
162   private final ControllerState       roomAreaOffsetState;
163   private final ControllerState       roomAreaRotationState;
164   private final ControllerState       roomNameOffsetState;
165   private final ControllerState       roomNameRotationState;
166   private final ControllerState       polylineCreationState;
167   private final ControllerState       polylineDrawingState;
168   private final ControllerState       polylineResizeState;
169   private final ControllerState       labelCreationState;
170   private final ControllerState       labelRotationState;
171   private final ControllerState       labelElevationState;
172   private final ControllerState       compassRotationState;
173   private final ControllerState       compassResizeState;
174   // Current state
175   private ControllerState             state;
176   private ControllerState             previousState;
177   // Mouse cursor position at last mouse press
178   private float                           xLastMousePress;
179   private float                           yLastMousePress;
180   private boolean                         shiftDownLastMousePress;
181   private boolean                         alignmentActivatedLastMousePress;
182   private boolean                         duplicationActivatedLastMousePress;
183   private boolean                         magnetismToggledLastMousePress;
184   private View.PointerType                pointerTypeLastMousePress;
185   private float                           xLastMouseMove;
186   private float                           yLastMouseMove;
187   private Area                            wallsAreaCache;
188   private Area                            wallsIncludingBaseboardsAreaCache;
189   private Area                            insideWallsAreaCache;
190   private List<GeneralPath>               roomPathsCache;
191   private Map<HomePieceOfFurniture, Area> furnitureSidesCache;
192   private List<Selectable>                draggedItems;
193 
194 
195 
196   /**
197    * Creates the controller of plan view.
198    * @param home        the home plan edited by this controller and its view
199    * @param preferences the preferences of the application
200    * @param viewFactory a factory able to create the plan view managed by this controller
201    * @param contentManager a content manager used to import furniture
202    * @param undoSupport undo support to post changes on plan by this controller
203    */
PlanController(Home home, UserPreferences preferences, ViewFactory viewFactory, ContentManager contentManager, UndoableEditSupport undoSupport)204   public PlanController(Home home,
205                         UserPreferences preferences,
206                         ViewFactory viewFactory,
207                         ContentManager contentManager,
208                         UndoableEditSupport undoSupport) {
209     super(home, preferences, viewFactory, contentManager, undoSupport);
210     this.home = home;
211     this.preferences = preferences;
212     this.viewFactory = viewFactory;
213     this.contentManager = contentManager;
214     this.undoSupport = undoSupport;
215     this.propertyChangeSupport = new PropertyChangeSupport(this);
216     this.furnitureSidesCache = new Hashtable<HomePieceOfFurniture, Area>();
217     // Initialize states
218     this.selectionState = new SelectionState();
219     this.selectionMoveState = new SelectionMoveState();
220     this.rectangleSelectionState = new RectangleSelectionState();
221     this.panningState = new PanningState();
222     this.dragAndDropState = new DragAndDropState();
223     this.wallCreationState = new WallCreationState();
224     this.wallDrawingState = new WallDrawingState();
225     this.wallResizeState = new WallResizeState();
226     this.wallArcExtentState = new WallArcExtentState();
227     this.pieceOfFurnitureRotationState = new PieceOfFurnitureRotationState();
228     this.pieceOfFurniturePitchRotationState = new PieceOfFurniturePitchRotationState();
229     this.pieceOfFurnitureRollRotationState = new PieceOfFurnitureRollRotationState();
230     this.pieceOfFurnitureElevationState = new PieceOfFurnitureElevationState();
231     this.pieceOfFurnitureHeightState = new PieceOfFurnitureHeightState();
232     this.pieceOfFurnitureResizeState = new PieceOfFurnitureResizeState();
233     this.lightPowerModificationState = new LightPowerModificationState();
234     this.pieceOfFurnitureNameOffsetState = new PieceOfFurnitureNameOffsetState();
235     this.pieceOfFurnitureNameRotationState = new PieceOfFurnitureNameRotationState();
236     this.cameraYawRotationState = new CameraYawRotationState();
237     this.cameraPitchRotationState = new CameraPitchRotationState();
238     this.cameraElevationState = new CameraElevationState();
239     this.dimensionLineCreationState = new DimensionLineCreationState();
240     this.dimensionLineDrawingState = new DimensionLineDrawingState();
241     this.dimensionLineResizeState = new DimensionLineResizeState();
242     this.dimensionLineOffsetState = new DimensionLineOffsetState();
243     this.roomCreationState = new RoomCreationState();
244     this.roomDrawingState = new RoomDrawingState();
245     this.roomResizeState = new RoomResizeState();
246     this.roomAreaOffsetState = new RoomAreaOffsetState();
247     this.roomAreaRotationState = new RoomAreaRotationState();
248     this.roomNameOffsetState = new RoomNameOffsetState();
249     this.roomNameRotationState = new RoomNameRotationState();
250     this.polylineCreationState = new PolylineCreationState();
251     this.polylineDrawingState = new PolylineDrawingState();
252     this.polylineResizeState = new PolylineResizeState();
253     this.labelCreationState = new LabelCreationState();
254     this.labelRotationState = new LabelRotationState();
255     this.labelElevationState = new LabelElevationState();
256     this.compassRotationState = new CompassRotationState();
257     this.compassResizeState = new CompassResizeState();
258     // Set default state to selectionState
259     setState(this.selectionState);
260 
261     addModelListeners();
262 
263     // Restore previous scale if it exists
264     Number scale = home.getNumericProperty(SCALE_VISUAL_PROPERTY);
265     if (scale != null) {
266       setScale(scale.floatValue());
267     }
268   }
269 
270   /**
271    * Returns the view associated with this controller.
272    */
getView()273   public PlanView getView() {
274     // Create view lazily only once it's needed
275     if (this.planView == null) {
276       this.planView = this.viewFactory.createPlanView(this.home, this.preferences, this);
277     }
278     return this.planView;
279   }
280 
281   /**
282    * Changes current state of controller.
283    */
setState(ControllerState state)284   protected void setState(ControllerState state) {
285     Mode oldMode = null;
286     boolean oldModificationState = false;
287     boolean oldBasePlanModificationState = false;
288     if (this.state != null) {
289       oldMode = this.state.getMode();
290       oldModificationState = this.state.isModificationState();
291       oldBasePlanModificationState = this.state.isBasePlanModificationState();
292       this.state.exit();
293     }
294 
295     this.previousState = this.state;
296     this.state = state;
297     this.state.enter();
298 
299     if (oldMode != state.getMode()) {
300       this.propertyChangeSupport.firePropertyChange(Property.MODE.name(),
301           oldMode, state.getMode());
302     }
303     if (oldModificationState != state.isModificationState()) {
304       this.propertyChangeSupport.firePropertyChange(Property.MODIFICATION_STATE.name(),
305           oldModificationState, !oldModificationState);
306     }
307     if (oldBasePlanModificationState != state.isBasePlanModificationState()) {
308       this.propertyChangeSupport.firePropertyChange(Property.BASE_PLAN_MODIFICATION_STATE.name(),
309           oldBasePlanModificationState, !oldBasePlanModificationState);
310     }
311   }
312 
313   /**
314    * Adds the property change <code>listener</code> in parameter to this controller.
315    */
addPropertyChangeListener(Property property, PropertyChangeListener listener)316   public void addPropertyChangeListener(Property property, PropertyChangeListener listener) {
317     this.propertyChangeSupport.addPropertyChangeListener(property.name(), listener);
318   }
319 
320   /**
321    * Removes the property change <code>listener</code> in parameter from this controller.
322    */
removePropertyChangeListener(Property property, PropertyChangeListener listener)323   public void removePropertyChangeListener(Property property, PropertyChangeListener listener) {
324     this.propertyChangeSupport.removePropertyChangeListener(property.name(), listener);
325   }
326 
327   /**
328    * Returns the active mode of this controller.
329    */
getMode()330   public Mode getMode() {
331     return this.state.getMode();
332   }
333 
334   /**
335    * Sets the active mode of this controller and fires a <code>PropertyChangeEvent</code>.
336    */
setMode(Mode mode)337   public void setMode(Mode mode) {
338     Mode oldMode = this.state.getMode();
339     if (mode != oldMode) {
340       this.state.setMode(mode);
341       this.propertyChangeSupport.firePropertyChange(Property.MODE.name(), oldMode, mode);
342     }
343   }
344 
345   /**
346    * Returns <code>true</code> if the interactions in the current mode may modify
347    * the state of a home.
348    */
isModificationState()349   public boolean isModificationState() {
350     return this.state.isModificationState();
351   }
352 
353   /**
354    * Returns <code>true</code> if the interactions in the current mode may modify
355    * the base plan of a home.
356    */
isBasePlanModificationState()357   public boolean isBasePlanModificationState() {
358     return this.state.isBasePlanModificationState();
359   }
360 
361   /**
362    * Deletes the selection in home.
363    */
364   @Override
deleteSelection()365   public void deleteSelection() {
366     this.state.deleteSelection();
367   }
368 
369   /**
370    * Escapes of current action.
371    */
escape()372   public void escape() {
373     this.state.escape();
374   }
375 
376   /**
377    * Moves the selection of (<code>dx</code>,<code>dy</code>) in home.
378    */
moveSelection(float dx, float dy)379   public void moveSelection(float dx, float dy) {
380     this.state.moveSelection(dx, dy);
381   }
382 
383   /**
384    * Toggles temporary magnetism feature of user preferences.
385    * @param magnetismToggled if <code>true</code> then magnetism feature is toggled.
386    */
toggleMagnetism(boolean magnetismToggled)387   public void toggleMagnetism(boolean magnetismToggled) {
388     this.state.toggleMagnetism(magnetismToggled);
389   }
390 
391   /**
392    * Activates or deactivates alignment feature.
393    * @param alignmentActivated if <code>true</code> then alignment is active.
394    * @since 4.0
395    */
setAlignmentActivated(boolean alignmentActivated)396   public void setAlignmentActivated(boolean alignmentActivated) {
397     this.state.setAlignmentActivated(alignmentActivated);
398   }
399 
400   /**
401    * Activates or deactivates duplication feature.
402    * @param duplicationActivated if <code>true</code> then duplication is active.
403    */
setDuplicationActivated(boolean duplicationActivated)404   public void setDuplicationActivated(boolean duplicationActivated) {
405     this.state.setDuplicationActivated(duplicationActivated);
406   }
407 
408   /**
409    * Activates or deactivates edition.
410    * @param editionActivated if <code>true</code> then edition is active
411    */
setEditionActivated(boolean editionActivated)412   public void setEditionActivated(boolean editionActivated) {
413     this.state.setEditionActivated(editionActivated);
414   }
415 
416   /**
417    * Updates an editable property with the entered <code>value</code>.
418    */
updateEditableProperty(EditableProperty editableProperty, Object value)419   public void updateEditableProperty(EditableProperty editableProperty, Object value) {
420     this.state.updateEditableProperty(editableProperty, value);
421   }
422 
423   /**
424    * Processes a mouse button pressed event.
425    */
pressMouse(float x, float y, int clickCount, boolean shiftDown, boolean duplicationActivated)426   public void pressMouse(float x, float y, int clickCount,
427                          boolean shiftDown, boolean duplicationActivated) {
428     pressMouse(x, y, clickCount, shiftDown, shiftDown, duplicationActivated, shiftDown);
429   }
430 
431   /**
432    * Processes a mouse button pressed event.
433    * @since 4.0
434    */
pressMouse(float x, float y, int clickCount, boolean shiftDown, boolean alignmentActivated, boolean duplicationActivated, boolean magnetismToggled)435   public void pressMouse(float x, float y, int clickCount, boolean shiftDown,
436                          boolean alignmentActivated, boolean duplicationActivated, boolean magnetismToggled) {
437     pressMouse(x, y, clickCount, shiftDown, alignmentActivated, duplicationActivated, magnetismToggled, null);
438   }
439 
440   /**
441    * Processes a mouse button pressed event.
442    * @since 6.4
443    */
pressMouse(float x, float y, int clickCount, boolean shiftDown, boolean alignmentActivated, boolean duplicationActivated, boolean magnetismToggled, View.PointerType pointerType)444   public void pressMouse(float x, float y, int clickCount, boolean shiftDown,
445                          boolean alignmentActivated, boolean duplicationActivated, boolean magnetismToggled,
446                          View.PointerType pointerType) {
447     // Store the last coordinates of a mouse press
448     this.xLastMousePress = x;
449     this.yLastMousePress = y;
450     this.xLastMouseMove = x;
451     this.yLastMouseMove = y;
452     this.shiftDownLastMousePress = shiftDown;
453     this.alignmentActivatedLastMousePress = alignmentActivated;
454     this.duplicationActivatedLastMousePress = duplicationActivated;
455     this.pointerTypeLastMousePress = pointerType;
456     this.magnetismToggledLastMousePress = magnetismToggled;
457     this.state.pressMouse(x, y, clickCount, shiftDown, duplicationActivated);
458   }
459 
460   /**
461    * Processes a mouse button released event.
462    */
releaseMouse(float x, float y)463   public void releaseMouse(float x, float y) {
464     this.state.releaseMouse(x, y);
465   }
466 
467   /**
468    * Processes a mouse button moved event.
469    */
moveMouse(float x, float y)470   public void moveMouse(float x, float y) {
471     // Store the last coordinates of a mouse move
472     this.xLastMouseMove = x;
473     this.yLastMouseMove = y;
474     this.state.moveMouse(x, y);
475   }
476 
477   /**
478    * Processes a zoom event.
479    */
zoom(float factor)480   public void zoom(float factor) {
481     this.state.zoom(factor);
482   }
483 
484   /**
485    * Returns the selection state.
486    */
getSelectionState()487   protected ControllerState getSelectionState() {
488     return this.selectionState;
489   }
490 
491   /**
492    * Returns the selection move state.
493    */
getSelectionMoveState()494   protected ControllerState getSelectionMoveState() {
495     return this.selectionMoveState;
496   }
497 
498   /**
499    * Returns the rectangle selection state.
500    */
getRectangleSelectionState()501   protected ControllerState getRectangleSelectionState() {
502     return this.rectangleSelectionState;
503   }
504 
505   /**
506    * Returns the panning state.
507    */
getPanningState()508   protected ControllerState getPanningState() {
509     return this.panningState;
510   }
511 
512   /**
513    * Returns the drag and drop state.
514    */
getDragAndDropState()515   protected ControllerState getDragAndDropState() {
516     return this.dragAndDropState;
517   }
518 
519   /**
520    * Returns the wall creation state.
521    */
getWallCreationState()522   protected ControllerState getWallCreationState() {
523     return this.wallCreationState;
524   }
525 
526   /**
527    * Returns the wall drawing state.
528    */
getWallDrawingState()529   protected ControllerState getWallDrawingState() {
530     return this.wallDrawingState;
531   }
532 
533   /**
534    * Returns the wall resize state.
535    */
getWallResizeState()536   protected ControllerState getWallResizeState() {
537     return this.wallResizeState;
538   }
539 
540   /**
541    * Returns the wall arc extent state.
542    * @since 6.0
543    */
getWallArcExtentState()544   protected ControllerState getWallArcExtentState() {
545     return this.wallArcExtentState;
546   }
547 
548   /**
549    * Returns the piece rotation state.
550    */
getPieceOfFurnitureRotationState()551   protected ControllerState getPieceOfFurnitureRotationState() {
552     return this.pieceOfFurnitureRotationState;
553   }
554 
555   /**
556    * Returns the piece pitch rotation state.
557    */
getPieceOfFurniturePitchRotationState()558   protected ControllerState getPieceOfFurniturePitchRotationState() {
559     return this.pieceOfFurniturePitchRotationState;
560   }
561 
562   /**
563    * Returns the piece roll rotation state.
564    */
getPieceOfFurnitureRollRotationState()565   protected ControllerState getPieceOfFurnitureRollRotationState() {
566     return this.pieceOfFurnitureRollRotationState;
567   }
568 
569   /**
570    * Returns the piece elevation state.
571    */
getPieceOfFurnitureElevationState()572   protected ControllerState getPieceOfFurnitureElevationState() {
573     return this.pieceOfFurnitureElevationState;
574   }
575 
576   /**
577    * Returns the piece height state.
578    */
getPieceOfFurnitureHeightState()579   protected ControllerState getPieceOfFurnitureHeightState() {
580     return this.pieceOfFurnitureHeightState;
581   }
582 
583   /**
584    * Returns the piece resize state.
585    */
getPieceOfFurnitureResizeState()586   protected ControllerState getPieceOfFurnitureResizeState() {
587     return this.pieceOfFurnitureResizeState;
588   }
589 
590   /**
591    * Returns the light power modification state.
592    */
getLightPowerModificationState()593   protected ControllerState getLightPowerModificationState() {
594     return this.lightPowerModificationState;
595   }
596 
597   /**
598    * Returns the piece name offset state.
599    */
getPieceOfFurnitureNameOffsetState()600   protected ControllerState getPieceOfFurnitureNameOffsetState() {
601     return this.pieceOfFurnitureNameOffsetState;
602   }
603 
604   /**
605    * Returns the piece name rotation state.
606    * @since 3.6
607    */
getPieceOfFurnitureNameRotationState()608   protected ControllerState getPieceOfFurnitureNameRotationState() {
609     return this.pieceOfFurnitureNameRotationState;
610   }
611 
612   /**
613    * Returns the camera yaw rotation state.
614    */
getCameraYawRotationState()615   protected ControllerState getCameraYawRotationState() {
616     return this.cameraYawRotationState;
617   }
618 
619   /**
620    * Returns the camera pitch rotation state.
621    */
getCameraPitchRotationState()622   protected ControllerState getCameraPitchRotationState() {
623     return this.cameraPitchRotationState;
624   }
625 
626   /**
627    * Returns the camera elevation state.
628    */
getCameraElevationState()629   protected ControllerState getCameraElevationState() {
630     return this.cameraElevationState;
631   }
632 
633   /**
634    * Returns the dimension line creation state.
635    */
getDimensionLineCreationState()636   protected ControllerState getDimensionLineCreationState() {
637     return this.dimensionLineCreationState;
638   }
639 
640   /**
641    * Returns the dimension line drawing state.
642    */
getDimensionLineDrawingState()643   protected ControllerState getDimensionLineDrawingState() {
644     return this.dimensionLineDrawingState;
645   }
646 
647   /**
648    * Returns the dimension line resize state.
649    */
getDimensionLineResizeState()650   protected ControllerState getDimensionLineResizeState() {
651     return this.dimensionLineResizeState;
652   }
653 
654   /**
655    * Returns the dimension line offset state.
656    */
getDimensionLineOffsetState()657   protected ControllerState getDimensionLineOffsetState() {
658     return this.dimensionLineOffsetState;
659   }
660 
661   /**
662    * Returns the room creation state.
663    */
getRoomCreationState()664   protected ControllerState getRoomCreationState() {
665     return this.roomCreationState;
666   }
667 
668   /**
669    * Returns the room drawing state.
670    */
getRoomDrawingState()671   protected ControllerState getRoomDrawingState() {
672     return this.roomDrawingState;
673   }
674 
675   /**
676    * Returns the room resize state.
677    */
getRoomResizeState()678   protected ControllerState getRoomResizeState() {
679     return this.roomResizeState;
680   }
681 
682   /**
683    * Returns the room area offset state.
684    */
getRoomAreaOffsetState()685   protected ControllerState getRoomAreaOffsetState() {
686     return this.roomAreaOffsetState;
687   }
688 
689   /**
690    * Returns the room area rotation state.
691    * @since 3.6
692    */
getRoomAreaRotationState()693   protected ControllerState getRoomAreaRotationState() {
694     return this.roomAreaRotationState;
695   }
696 
697   /**
698    * Returns the room name offset state.
699    */
getRoomNameOffsetState()700   protected ControllerState getRoomNameOffsetState() {
701     return this.roomNameOffsetState;
702   }
703 
704   /**
705    * Returns the room name rotation state.
706    * @since 3.6
707    */
getRoomNameRotationState()708   protected ControllerState getRoomNameRotationState() {
709     return this.roomNameRotationState;
710   }
711 
712   /**
713    * Returns the polyline creation state.
714    * @since 5.0
715    */
getPolylineCreationState()716   protected ControllerState getPolylineCreationState() {
717     return this.polylineCreationState;
718   }
719 
720   /**
721    * Returns the polyline drawing state.
722    * @since 5.0
723    */
getPolylineDrawingState()724   protected ControllerState getPolylineDrawingState() {
725     return this.polylineDrawingState;
726   }
727 
728   /**
729    * Returns the polyline resize state.
730    * @since 5.0
731    */
getPolylineResizeState()732   protected ControllerState getPolylineResizeState() {
733     return this.polylineResizeState;
734   }
735 
736   /**
737    * Returns the label creation state.
738    */
getLabelCreationState()739   protected ControllerState getLabelCreationState() {
740     return this.labelCreationState;
741   }
742 
743   /**
744    * Returns the label rotation state.
745    * @since 3.6
746    */
getLabelRotationState()747   protected ControllerState getLabelRotationState() {
748     return this.labelRotationState;
749   }
750 
751   /**
752    * Returns the label elevation state.
753    * @since 5.0
754    */
getLabelElevationState()755   protected ControllerState getLabelElevationState() {
756     return this.labelElevationState;
757   }
758 
759   /**
760    * Returns the compass rotation state.
761    */
getCompassRotationState()762   protected ControllerState getCompassRotationState() {
763     return this.compassRotationState;
764   }
765 
766   /**
767    * Returns the compass resize state.
768    */
getCompassResizeState()769   protected ControllerState getCompassResizeState() {
770     return this.compassResizeState;
771   }
772 
773   /**
774    * Returns the abscissa of mouse position at last mouse press.
775    */
getXLastMousePress()776   protected float getXLastMousePress() {
777     return this.xLastMousePress;
778   }
779 
780   /**
781    * Returns the ordinate of mouse position at last mouse press.
782    */
getYLastMousePress()783   protected float getYLastMousePress() {
784     return this.yLastMousePress;
785   }
786 
787   /**
788    * Returns <code>true</code> if shift key was down at last mouse press.
789    */
wasShiftDownLastMousePress()790   protected boolean wasShiftDownLastMousePress() {
791     return this.shiftDownLastMousePress;
792   }
793 
794   /**
795    * Returns <code>true</code> if magnetism was toggled at last mouse press.
796    * @since 4.0
797    */
wasMagnetismToggledLastMousePress()798   protected boolean wasMagnetismToggledLastMousePress() {
799     return this.magnetismToggledLastMousePress;
800   }
801 
802   /**
803    * Returns <code>true</code> if alignment was activated at last mouse press.
804    * @since 4.0
805    */
wasAlignmentActivatedLastMousePress()806   protected boolean wasAlignmentActivatedLastMousePress() {
807     return this.alignmentActivatedLastMousePress;
808   }
809 
810   /**
811    * Returns <code>true</code> if duplication was activated at last mouse press.
812    */
wasDuplicationActivatedLastMousePress()813   protected boolean wasDuplicationActivatedLastMousePress() {
814     return this.duplicationActivatedLastMousePress;
815   }
816 
817   /**
818    * Returns the pointer type used at the last mouse press.
819    * @since 6.4
820    */
getPointerTypeLastMousePress()821   protected View.PointerType getPointerTypeLastMousePress() {
822     return this.pointerTypeLastMousePress;
823   }
824 
825   /**
826    * Returns the abscissa of mouse position at last mouse move.
827    */
getXLastMouseMove()828   protected float getXLastMouseMove() {
829     return this.xLastMouseMove;
830   }
831 
832   /**
833    * Returns the ordinate of mouse position at last mouse move.
834    */
getYLastMouseMove()835   protected float getYLastMouseMove() {
836     return this.yLastMouseMove;
837   }
838 
839   /**
840    * Controls the modification of selected walls.
841    */
modifySelectedWalls()842   public void modifySelectedWalls() {
843     if (!Home.getWallsSubList(this.home.getSelectedItems()).isEmpty()) {
844       new WallController(this.home, this.preferences, this.viewFactory,
845           this.contentManager, this.undoSupport).displayView(getView());
846     }
847   }
848 
849   /**
850    * Locks home base plan.
851    */
lockBasePlan()852   public void lockBasePlan() {
853     if (!this.home.isBasePlanLocked()) {
854       final boolean allLevelsSelection = this.home.isAllLevelsSelection();
855       List<Selectable> selection = this.home.getSelectedItems();
856       final Selectable [] oldSelection =
857           selection.toArray(new Selectable [selection.size()]);
858 
859       List<Selectable> newSelection = getItemsNotPartOfBasePlan(selection);
860       final Selectable [] newSelectedItems =
861         newSelection.toArray(new Selectable [newSelection.size()]);
862 
863       this.home.setBasePlanLocked(true);
864       selectItems(newSelection, allLevelsSelection);
865       this.undoSupport.postEdit(new LockingUndoableEdit(this, this.home, this.preferences,
866           oldSelection, allLevelsSelection, newSelectedItems));
867     }
868   }
869 
870   /**
871    * Undoable edit for plan locking.
872    */
873   private static class LockingUndoableEdit extends LocalizedUndoableEdit {
874     private final PlanController controller;
875     private final Home           home;
876     private final Selectable []  oldSelection;
877     private final boolean        allLevelsSelection;
878     private final Selectable []  newSelectedItems;
879 
LockingUndoableEdit(PlanController controller, Home home, UserPreferences preferences, Selectable [] oldSelection, boolean allLevelsSelection, Selectable [] newSelectedItems)880     public LockingUndoableEdit(PlanController controller, Home home, UserPreferences preferences,
881                                Selectable [] oldSelection, boolean allLevelsSelection,
882                                Selectable [] newSelectedItems) {
883       super(preferences, PlanController.class, "undoLockBasePlan");
884       this.controller = controller;
885       this.home = home;
886       this.oldSelection = oldSelection;
887       this.allLevelsSelection = allLevelsSelection;
888       this.newSelectedItems = newSelectedItems;
889     }
890 
891     @Override
undo()892     public void undo() throws CannotUndoException {
893       super.undo();
894       this.home.setBasePlanLocked(false);
895       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.allLevelsSelection);
896     }
897 
898     @Override
redo()899     public void redo() throws CannotRedoException {
900       super.redo();
901       this.home.setBasePlanLocked(true);
902       this.controller.selectAndShowItems(Arrays.asList(this.newSelectedItems), this.allLevelsSelection);
903     }
904   }
905 
906   /**
907    * Returns <code>true</code> it the given <code>item</code> belongs
908    * to the base plan.
909    */
isItemPartOfBasePlan(Selectable item)910   protected boolean isItemPartOfBasePlan(Selectable item) {
911     if (item instanceof HomePieceOfFurniture) {
912       return isPieceOfFurniturePartOfBasePlan((HomePieceOfFurniture)item);
913     } else {
914       return !(item instanceof ObserverCamera);
915     }
916   }
917 
918   /**
919    * Returns the items among the given list that are not part of the base plan.
920    */
getItemsNotPartOfBasePlan(List<? extends Selectable> items)921   private List<Selectable> getItemsNotPartOfBasePlan(List<? extends Selectable> items) {
922     List<Selectable> itemsNotPartOfBasePlan = new ArrayList<Selectable>();
923     for (Selectable item : items) {
924       if (!isItemPartOfBasePlan(item)) {
925         itemsNotPartOfBasePlan.add(item);
926       }
927     }
928     return itemsNotPartOfBasePlan;
929   }
930 
931   /**
932    * Unlocks home base plan.
933    */
unlockBasePlan()934   public void unlockBasePlan() {
935     if (this.home.isBasePlanLocked()) {
936       final boolean allLevelsSelection = this.home.isAllLevelsSelection();
937       List<Selectable> selection = this.home.getSelectedItems();
938       final Selectable [] selectedItems =
939           selection.toArray(new Selectable [selection.size()]);
940 
941       this.home.setBasePlanLocked(false);
942       this.home.setAllLevelsSelection(false);
943       this.undoSupport.postEdit(new UnlockingUndoableEdit(this, this.home, this.preferences,
944           selectedItems, allLevelsSelection));
945     }
946   }
947 
948   /**
949    * Undoable edit for plan unlocking.
950    */
951   private static class UnlockingUndoableEdit extends LocalizedUndoableEdit {
952     private final PlanController controller;
953     private final Home           home;
954     private final Selectable []  selectedItems;
955     private final boolean        allLevelsSelection;
956 
UnlockingUndoableEdit(PlanController controller, Home home, UserPreferences preferences, Selectable [] selectedItems, boolean allLevelsSelection)957     public UnlockingUndoableEdit(PlanController controller, Home home, UserPreferences preferences,
958                                  Selectable [] selectedItems, boolean allLevelsSelection) {
959       super(preferences, PlanController.class, "undoUnlockBasePlan");
960       this.controller = controller;
961       this.home = home;
962       this.selectedItems = selectedItems;
963       this.allLevelsSelection = allLevelsSelection;
964     }
965 
966     @Override
undo()967     public void undo() throws CannotUndoException {
968       super.undo();
969       this.home.setBasePlanLocked(true);
970       this.controller.selectAndShowItems(Arrays.asList(this.selectedItems), this.allLevelsSelection);
971     }
972 
973     @Override
redo()974     public void redo() throws CannotRedoException {
975       super.redo();
976       this.home.setBasePlanLocked(false);
977       this.controller.selectAndShowItems(Arrays.asList(this.selectedItems), false);
978     }
979   }
980 
981   /**
982    * Returns <code>true</code> if the given <code>item</code> may be moved
983    * in the plan. Default implementation returns <code>true</code>.
984    */
isItemMovable(Selectable item)985   protected boolean isItemMovable(Selectable item) {
986     if (item instanceof HomePieceOfFurniture) {
987       return isPieceOfFurnitureMovable((HomePieceOfFurniture)item);
988     } else {
989       return true;
990     }
991   }
992 
993   /**
994    * Returns <code>true</code> if the given <code>item</code> may be resized.
995    * Default implementation returns <code>false</code> if the given <code>item</code>
996    * is a non resizable piece of furniture.
997    */
isItemResizable(Selectable item)998   protected boolean isItemResizable(Selectable item) {
999     if (item instanceof HomePieceOfFurniture) {
1000       return ((HomePieceOfFurniture)item).isResizable();
1001     } else {
1002       return true;
1003     }
1004   }
1005 
1006   /**
1007    * Returns <code>true</code> if the given <code>item</code> may be deleted.
1008    * Default implementation returns <code>true</code> except if the given <code>item</code>
1009    * is a camera or a compass or if the given <code>item</code> isn't a
1010    * {@linkplain #isPieceOfFurnitureDeletable(HomePieceOfFurniture) deletable piece of furniture}.
1011    */
isItemDeletable(Selectable item)1012   protected boolean isItemDeletable(Selectable item) {
1013     if (item instanceof HomePieceOfFurniture) {
1014       return isPieceOfFurnitureDeletable((HomePieceOfFurniture)item);
1015     } else {
1016       return !(item instanceof Compass || item instanceof Camera);
1017     }
1018   }
1019 
1020   /**
1021    * Flips horizontally selected objects.
1022    * @since 6.0
1023    */
flipHorizontally()1024   public void flipHorizontally() {
1025     flipSelectedItems(true);
1026   }
1027 
1028   /**
1029    * Flips vertically selected objects.
1030    * @since 6.0
1031    */
flipVertically()1032   public void flipVertically() {
1033     flipSelectedItems(false);
1034   }
1035 
flipSelectedItems(boolean horizontally)1036   private void flipSelectedItems(boolean horizontally) {
1037     List<Selectable> selectedItems = this.home.getSelectedItems();
1038     if (!selectedItems.isEmpty()) {
1039       Selectable [] flippedItems = selectedItems.toArray(new Selectable [selectedItems.size()]);
1040       // Compute text base offsets to avoid using plan view during undo / redo
1041       float [][] itemTextBaseOffsets = new float [flippedItems.length][];
1042       for (int i = 0; i < itemTextBaseOffsets.length; i++) {
1043         if (flippedItems [i] instanceof HomeFurnitureGroup) {
1044           HomeFurnitureGroup group = (HomeFurnitureGroup)flippedItems [i];
1045           List<HomePieceOfFurniture> furniture = group.getAllFurniture();
1046           itemTextBaseOffsets [i] = new float [furniture.size() + 1];
1047           itemTextBaseOffsets [i][0] = getTextBaseOffset(group.getName(), group.getNameStyle(), group.getClass());
1048           for (int j = 0; j < furniture.size(); j++) {
1049             HomePieceOfFurniture piece = furniture.get(j);
1050             itemTextBaseOffsets [i][j + 1] = getTextBaseOffset(piece.getName(), piece.getNameStyle(), piece.getClass());
1051           }
1052         } else if (flippedItems [i] instanceof HomePieceOfFurniture) {
1053           HomePieceOfFurniture piece = (HomePieceOfFurniture)flippedItems [i];
1054           itemTextBaseOffsets [i] = new float [] {getTextBaseOffset(piece.getName(), piece.getNameStyle(), piece.getClass())};
1055         } else if (flippedItems [i] instanceof Room) {
1056           Room room = (Room)flippedItems [i];
1057           itemTextBaseOffsets [i] = new float [] {
1058               getTextBaseOffset(room.getName(), room.getNameStyle(), room.getClass()),
1059               getTextBaseOffset(this.preferences.getLengthUnit().getAreaFormatWithUnit().format(room.getArea()), room.getAreaStyle(), room.getClass())};
1060         }
1061       }
1062       doFlipItems(flippedItems, itemTextBaseOffsets, horizontally);
1063       selectAndShowItems(selectedItems, this.home.isAllLevelsSelection());
1064       this.undoSupport.postEdit(new FlippingUndoableEdit(this, this.preferences, this.home.isAllLevelsSelection(),
1065           selectedItems.toArray(new Selectable [selectedItems.size()]), itemTextBaseOffsets, horizontally));
1066     }
1067   }
1068 
1069   /**
1070    * Undoable edit for flipped items.
1071    */
1072   private static class FlippingUndoableEdit extends LocalizedUndoableEdit {
1073     private final PlanController controller;
1074     private final boolean        allLevelsSelection;
1075     private final Selectable []  items;
1076     private final float [][]     itemTextBaseOffsets;
1077     private final boolean        horizontalFlip;
1078 
FlippingUndoableEdit(PlanController controller, UserPreferences preferences, boolean allLevelsSelection, Selectable [] items, float [][] itemTextBaseOffsets, boolean horizontalFlip)1079     public FlippingUndoableEdit(PlanController controller, UserPreferences preferences,
1080                                 boolean allLevelsSelection, Selectable [] items,
1081                                 float [][] itemTextBaseOffsets, boolean horizontalFlip) {
1082       super(preferences, PlanController.class, "undoFlipName");
1083       this.controller = controller;
1084       this.allLevelsSelection = allLevelsSelection;
1085       this.items = items;
1086       this.itemTextBaseOffsets = itemTextBaseOffsets;
1087       this.horizontalFlip = horizontalFlip;
1088     }
1089 
1090     @Override
undo()1091     public void undo() throws CannotUndoException {
1092       super.undo();
1093       this.controller.doFlipItems(this.items, this.itemTextBaseOffsets, this.horizontalFlip);
1094       this.controller.selectAndShowItems(Arrays.asList(this.items), this.allLevelsSelection);
1095     }
1096 
1097     @Override
redo()1098     public void redo() throws CannotRedoException {
1099       super.redo();
1100       this.controller.doFlipItems(this.items, this.itemTextBaseOffsets, this.horizontalFlip);
1101       this.controller.selectAndShowItems(Arrays.asList(this.items), this.allLevelsSelection);
1102     }
1103   }
1104 
1105   /**
1106    * Flips the <code>items</code>.
1107    */
doFlipItems(Selectable [] items, float [][] itemTextBaseOffsets, boolean horizontalFlip)1108   private void doFlipItems(Selectable [] items, float [][] itemTextBaseOffsets, boolean horizontalFlip) {
1109     float minX = Float.MAX_VALUE;
1110     float minY = Float.MAX_VALUE;
1111     float maxX = -Float.MAX_VALUE;
1112     float maxY = -Float.MAX_VALUE;
1113     for (Selectable item : items) {
1114       if (!(item instanceof ObserverCamera)) {
1115         for (float [] point : item.getPoints()) {
1116           minX = Math.min(minX, point [0]);
1117           minY = Math.min(minY, point [1]);
1118           maxX = Math.max(maxX, point [0]);
1119           maxY = Math.max(maxY, point [1]);
1120         }
1121       }
1122     }
1123     float symmetryX = (minX + maxX) / 2;
1124     float symmetryY = (minY + maxY) / 2;
1125     List<Selectable> flippedItems = Arrays.asList(items);
1126     for (int i = 0; i < items.length; i++) {
1127       flipItem(items [i], itemTextBaseOffsets [i], 0,
1128           horizontalFlip ? symmetryX : symmetryY, horizontalFlip, flippedItems);
1129     }
1130   }
1131 
1132   /**
1133    * Flips the given <code>item</code> with the given axis coordinate.
1134    * @param item the item to flip
1135    * @param itemTextBaseOffsets base offset for the texts of the item
1136    * @param offsetIndex  index to get the first text base offset of item
1137    * @param axisCoordinate the coordinate of the symmetry axis
1138    * @param horizontalFlip if <code>true</code> the item should be flipped horizontally otherwise vertically
1139    * @param flippedItems list of all the items that must be flipped
1140    * @since 6.0
1141    */
flipItem(Selectable item, float [] itemTextBaseOffsets, int offsetIndex, float axisCoordinate, boolean horizontalFlip, List<Selectable> flippedItems)1142   protected void flipItem(Selectable item, float [] itemTextBaseOffsets, int offsetIndex,
1143                           float axisCoordinate, boolean horizontalFlip, List<Selectable> flippedItems) {
1144     if (item instanceof HomePieceOfFurniture) {
1145       HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
1146       if (horizontalFlip) {
1147         piece.setX(axisCoordinate * 2 - piece.getX());
1148         piece.setAngle(-piece.getAngle());
1149         flipPieceOfFurnitureName(piece, itemTextBaseOffsets [0], horizontalFlip);
1150       } else {
1151         piece.setY(axisCoordinate * 2 - piece.getY());
1152         piece.setAngle((float)Math.PI - piece.getAngle());
1153         flipPieceOfFurnitureName(piece, itemTextBaseOffsets [0], horizontalFlip);
1154       }
1155       if (piece.isHorizontallyRotatable()) {
1156         piece.setRoll(-piece.getRoll());
1157       }
1158       if (piece.isResizable()) {
1159         piece.setModelMirrored(!piece.isModelMirrored());
1160       }
1161 
1162       if (item instanceof HomeFurnitureGroup) {
1163         List<HomePieceOfFurniture> furniture = ((HomeFurnitureGroup)item).getAllFurniture();
1164         for (int i = 0; i < furniture.size(); i++) {
1165           flipPieceOfFurnitureName(furniture.get(i), itemTextBaseOffsets [i + 1], horizontalFlip);
1166         }
1167       }
1168     } else if (item instanceof Wall) {
1169       Wall wall = (Wall)item;
1170       if (horizontalFlip) {
1171         wall.setXStart(axisCoordinate * 2 - wall.getXStart());
1172         Wall wallAtStart = wall.getWallAtStart();
1173         if (wallAtStart != null && !flippedItems.contains(wallAtStart)) {
1174           if (wallAtStart.getWallAtStart() == wall) {
1175             wallAtStart.setXStart(axisCoordinate * 2 - wallAtStart.getXStart());
1176           } else {
1177             wallAtStart.setXEnd(axisCoordinate * 2 - wallAtStart.getXEnd());
1178           }
1179         }
1180         wall.setXEnd(axisCoordinate * 2 - wall.getXEnd());
1181         Wall wallAtEnd = wall.getWallAtEnd();
1182         if (wallAtEnd != null && !flippedItems.contains(wallAtEnd)) {
1183           if (wallAtEnd.getWallAtStart() == wall) {
1184             wallAtEnd.setXStart(axisCoordinate * 2 - wallAtEnd.getXStart());
1185           } else {
1186             wallAtEnd.setXEnd(axisCoordinate * 2 - wallAtEnd.getXEnd());
1187           }
1188         }
1189       } else {
1190         wall.setYStart(axisCoordinate * 2 - wall.getYStart());
1191         Wall wallAtStart = wall.getWallAtStart();
1192         if (wallAtStart != null && !flippedItems.contains(wallAtStart)) {
1193           if (wallAtStart.getWallAtStart() == wall) {
1194             wallAtStart.setYStart(axisCoordinate * 2 - wallAtStart.getYStart());
1195           } else {
1196             wallAtStart.setYEnd(axisCoordinate * 2 - wallAtStart.getYEnd());
1197           }
1198         }
1199         wall.setYEnd(axisCoordinate * 2 - wall.getYEnd());
1200         Wall wallAtEnd = wall.getWallAtEnd();
1201         if (wallAtEnd != null && !flippedItems.contains(wallAtEnd)) {
1202           if (wallAtEnd.getWallAtStart() == wall) {
1203             wallAtEnd.setYStart(axisCoordinate * 2 - wallAtEnd.getYStart());
1204           } else {
1205             wallAtEnd.setYEnd(axisCoordinate * 2 - wallAtEnd.getYEnd());
1206           }
1207         }
1208       }
1209       Float arcExtent = wall.getArcExtent();
1210       if (arcExtent != null) {
1211         wall.setArcExtent(-arcExtent);
1212       }
1213       reverseWallSidesStyle(wall);
1214     } else if (item instanceof Room) {
1215       Room room = (Room)item;
1216       float [][] points = room.getPoints();
1217       for (float [] point : points) {
1218         if (horizontalFlip) {
1219           point [0] = axisCoordinate * 2 - point [0];
1220         } else {
1221           point [1] = axisCoordinate * 2 - point [1];
1222         }
1223       }
1224       room.setPoints(points);
1225       TextStyle nameStyle = room.getNameStyle();
1226       TextStyle areaStyle = room.getAreaStyle();
1227       if (horizontalFlip) {
1228         room.setNameXOffset(-room.getNameXOffset());
1229         room.setAreaXOffset(-room.getAreaXOffset());
1230         if (nameStyle != null) {
1231           if (nameStyle.getAlignment() == TextStyle.Alignment.LEFT) {
1232             room.setNameStyle(nameStyle.deriveStyle(TextStyle.Alignment.RIGHT));
1233           } else if (nameStyle.getAlignment() == TextStyle.Alignment.RIGHT) {
1234             room.setNameStyle(nameStyle.deriveStyle(TextStyle.Alignment.LEFT));
1235           }
1236         }
1237         if (areaStyle != null) {
1238           if (areaStyle.getAlignment() == TextStyle.Alignment.LEFT) {
1239             room.setAreaStyle(areaStyle.deriveStyle(TextStyle.Alignment.RIGHT));
1240           } else if (areaStyle.getAlignment() == TextStyle.Alignment.RIGHT) {
1241             room.setAreaStyle(areaStyle.deriveStyle(TextStyle.Alignment.LEFT));
1242           }
1243         }
1244       } else {
1245         room.setNameYOffset(-room.getNameYOffset());
1246         // Take into account font size
1247         float baseOffset = itemTextBaseOffsets [0];
1248         room.setNameXOffset(room.getNameXOffset() - baseOffset * (float)Math.sin(room.getNameAngle()));
1249         room.setNameYOffset(room.getNameYOffset() - baseOffset * (float)Math.cos(room.getNameAngle()));
1250 
1251         room.setAreaYOffset(-room.getAreaYOffset());
1252         baseOffset = itemTextBaseOffsets [1];
1253         room.setAreaXOffset(room.getAreaXOffset() - baseOffset * (float)Math.sin(room.getAreaAngle()));
1254         room.setAreaYOffset(room.getAreaYOffset() - baseOffset * (float)Math.cos(room.getAreaAngle()));
1255       }
1256       room.setNameAngle(-room.getNameAngle());
1257       room.setAreaAngle(-room.getAreaAngle());
1258     } else if (item instanceof Polyline) {
1259       Polyline polyline = (Polyline)item;
1260       float [][] points = polyline.getPoints();
1261       for (float [] point : points) {
1262         if (horizontalFlip) {
1263           point [0] = axisCoordinate * 2 - point [0];
1264         } else {
1265           point [1] = axisCoordinate * 2 - point [1];
1266         }
1267       }
1268       polyline.setPoints(points);
1269     } else if (item instanceof DimensionLine) {
1270       DimensionLine dimensionLine = (DimensionLine)item;
1271       if (horizontalFlip) {
1272         // Reverse also ends to keep same text orientation
1273         float xStart = dimensionLine.getXStart();
1274         dimensionLine.setXStart(axisCoordinate * 2 - dimensionLine.getXEnd());
1275         dimensionLine.setXEnd(axisCoordinate * 2 - xStart);
1276         float yStart = dimensionLine.getYStart();
1277         dimensionLine.setYStart(dimensionLine.getYEnd());
1278         dimensionLine.setYEnd(yStart);
1279       } else {
1280         dimensionLine.setYStart(axisCoordinate * 2 - dimensionLine.getYStart());
1281         dimensionLine.setYEnd(axisCoordinate * 2 - dimensionLine.getYEnd());
1282         dimensionLine.setOffset(-dimensionLine.getOffset());
1283       }
1284     } else if (item instanceof Label) {
1285       Label label = (Label)item;
1286       if (horizontalFlip) {
1287         label.setX(axisCoordinate * 2 - label.getX());
1288         label.setAngle(-label.getAngle());
1289       } else {
1290         label.setY(axisCoordinate * 2 - label.getY());
1291         if (label.getPitch() != null) {
1292           label.setAngle((float)Math.PI - label.getAngle());
1293         } else {
1294           label.setAngle(-label.getAngle());
1295         }
1296       }
1297       TextStyle style = label.getStyle();
1298       if (style != null) {
1299         if (style.getAlignment() == TextStyle.Alignment.LEFT) {
1300           label.setStyle(style.deriveStyle(TextStyle.Alignment.RIGHT));
1301         } else if (style.getAlignment() == TextStyle.Alignment.RIGHT) {
1302           label.setStyle(style.deriveStyle(TextStyle.Alignment.LEFT));
1303         }
1304       }
1305     } else if (item instanceof Compass) {
1306       Compass compass = (Compass)item;
1307       if (horizontalFlip) {
1308         compass.setX(axisCoordinate * 2 - compass.getX());
1309         compass.setNorthDirection(-compass.getNorthDirection());
1310       } else {
1311         compass.setY(axisCoordinate * 2 - compass.getY());
1312         compass.setNorthDirection((float)Math.PI - compass.getNorthDirection());
1313       }
1314     }
1315   }
1316 
1317   /**
1318    * Flips the name of the given <code>piece</code>.
1319    */
flipPieceOfFurnitureName(HomePieceOfFurniture piece, float nameBaseOffset, boolean horizontalFlip)1320   private static void flipPieceOfFurnitureName(HomePieceOfFurniture piece, float nameBaseOffset, boolean horizontalFlip) {
1321     if (horizontalFlip) {
1322       piece.setNameXOffset(-piece.getNameXOffset());
1323       TextStyle nameStyle = piece.getNameStyle();
1324       if (nameStyle != null) {
1325         if (nameStyle.getAlignment() == TextStyle.Alignment.LEFT) {
1326           piece.setNameStyle(nameStyle.deriveStyle(TextStyle.Alignment.RIGHT));
1327         } else if (nameStyle.getAlignment() == TextStyle.Alignment.RIGHT) {
1328           piece.setNameStyle(nameStyle.deriveStyle(TextStyle.Alignment.LEFT));
1329         }
1330       }
1331     } else {
1332       piece.setNameYOffset(-piece.getNameYOffset());
1333       if (piece.getNameXOffset() != 0 || piece.getNameYOffset() != 0) {
1334         // Take into account font size
1335         piece.setNameXOffset(piece.getNameXOffset() - nameBaseOffset * (float)Math.sin(piece.getNameAngle()));
1336         piece.setNameYOffset(piece.getNameYOffset() - nameBaseOffset * (float)Math.cos(piece.getNameAngle()));
1337       }
1338       piece.setNameAngle(-piece.getNameAngle());
1339     }
1340   }
1341 
1342   /**
1343    * Returns the offset between the vertical middle of the text and its base.
1344    */
getTextBaseOffset(String text, TextStyle textStyle, Class<? extends Selectable> itemClass)1345   private float getTextBaseOffset(String text, TextStyle textStyle, Class<? extends Selectable> itemClass) {
1346     if (textStyle == null) {
1347       textStyle = this.preferences.getDefaultTextStyle(itemClass);
1348     }
1349     float [][] textBounds = getView().getTextBounds(text != null ? text : "Ag", textStyle, 0, 0, 0);
1350     return (textBounds [textBounds.length - 1][1] + textBounds [0][1]) / 2;
1351   }
1352 
1353   /**
1354    * Controls how selected walls are joined.
1355    * @since 5.5
1356    */
joinSelectedWalls()1357   public void joinSelectedWalls() {
1358     List<Selectable> selectedItems = this.home.getSelectedItems();
1359     List<Wall> selectedWalls = Home.getWallsSubList(selectedItems);
1360     final Wall [] walls = {null, null};
1361     for (Wall wall : selectedWalls) {
1362       if ((wall.getArcExtent() == null
1363               || wall.getArcExtent() == 0f)
1364           && (wall.getWallAtStart() == null
1365               || wall.getWallAtEnd() == null)) {
1366         if (walls [0] == null) {
1367           walls [0] = wall;
1368         } else {
1369           walls [1] = wall;
1370           break;
1371         }
1372       }
1373     }
1374     if (walls [1] == null) {
1375       Collections.sort(selectedWalls, new Comparator<Wall>() {
1376           public int compare(Wall wall1, Wall wall2) {
1377             float[] intersection1 = computeIntersection(wall1.getXStart(), wall1.getYStart(), wall1.getXEnd(), wall1.getYEnd(),
1378                 walls [0].getXStart(), walls [0].getYStart(), walls [0].getXEnd(), walls [0].getYEnd());
1379             float[] intersection2 = computeIntersection(wall2.getXStart(), wall2.getYStart(), wall2.getXEnd(), wall2.getYEnd(),
1380                 walls [0].getXStart(), walls [0].getYStart(), walls [0].getXEnd(), walls [0].getYEnd());
1381             double closestPoint1 = Math.min(Point2D.distanceSq(walls [0].getXStart(), walls [0].getYStart(), intersection1 [0], intersection1 [1]),
1382                 Point2D.distanceSq(walls [0].getXEnd(), walls [0].getYEnd(), intersection1 [0], intersection1 [1]));
1383             double closestPoint2 = Math.min(Point2D.distanceSq(walls [0].getXStart(), walls [0].getYStart(), intersection2 [0], intersection2 [1]),
1384                 Point2D.distanceSq(walls [0].getXEnd(), walls [0].getYEnd(), intersection2 [0], intersection2 [1]));
1385             return Double.compare(closestPoint1, closestPoint2);
1386           }
1387         });
1388       if (walls [0] != selectedWalls.get(1)) {
1389         walls [1] = selectedWalls.get(1);
1390       }
1391     }
1392     if (walls [1] != null) {
1393       // Check parallelism 1 deg close
1394       double firstWallAngle = Math.atan2(walls [0].getYEnd() - walls [0].getYStart(),
1395             walls [0].getXEnd() - walls [0].getXStart());
1396       double secondWallAngle = Math.atan2(walls [1].getYEnd() - walls [1].getYStart(),
1397           walls [1].getXEnd() - walls [1].getXStart());
1398       double wallsAngle = Math.abs(firstWallAngle - secondWallAngle) % Math.PI;
1399       boolean parallel = wallsAngle <= Math.PI / 360 || (Math.PI - wallsAngle) <= Math.PI / 360;
1400       float[] joinPoint = null;
1401       if (!parallel) {
1402         joinPoint = computeIntersection(walls [0].getXStart(), walls [0].getYStart(), walls [0].getXEnd(), walls [0].getYEnd(),
1403             walls [1].getXStart(), walls [1].getYStart(), walls [1].getXEnd(), walls [1].getYEnd());
1404       } else if (Line2D.ptLineDistSq(walls [1].getXStart(), walls [1].getYStart(), walls [1].getXEnd(), walls [1].getYEnd(), walls [0].getXStart(), walls [0].getYStart()) < 1E-2
1405                  && Line2D.ptLineDistSq(walls [1].getXStart(), walls [1].getYStart(), walls [1].getXEnd(), walls [1].getYEnd(), walls [0].getXEnd(), walls [0].getYEnd()) < 1E-2) {
1406         // Search join point for walls in the same row
1407         if (walls [1].getWallAtStart() == null
1408             ^ walls [1].getWallAtEnd() == null) {
1409           // If second wall has only one free end, join the first wall to this free end
1410           if (walls [1].getWallAtStart() == null) {
1411             joinPoint = new float [] {walls [1].getXStart(), walls [1].getYStart()};
1412           } else {
1413             joinPoint = new float [] {walls [1].getXEnd(), walls [1].getYEnd()};
1414           }
1415         } else if (walls [1].getWallAtStart() == null
1416                    && walls [1].getWallAtEnd() == null) {
1417           double wallStartDistanceToSegment = Line2D.ptSegDistSq(walls [1].getXStart(), walls [1].getYStart(), walls [1].getXEnd(), walls [1].getYEnd(), walls [0].getXStart(), walls [0].getYStart());
1418           double wallEndDistanceToSegment = Line2D.ptSegDistSq(walls [1].getXStart(), walls [1].getYStart(), walls [1].getXEnd(), walls [1].getYEnd(), walls [0].getXEnd(), walls [0].getYEnd());
1419           if (wallStartDistanceToSegment > 1E-2
1420               && wallEndDistanceToSegment > 1E-2) {
1421             // If walls don't overlap, connect first wall to the closest point
1422             if (walls [0].getWallAtEnd() != null
1423                 || walls [0].getWallAtStart() == null
1424                    && wallStartDistanceToSegment <= wallEndDistanceToSegment) {
1425               if (Point2D.distanceSq(walls [1].getXStart(), walls [1].getYStart(), walls [0].getXStart(), walls [0].getYStart())
1426                   < Point2D.distanceSq(walls [1].getXEnd(), walls [1].getYEnd(), walls [0].getXStart(), walls [0].getYStart())) {
1427                 joinPoint = new float [] {walls [1].getXStart(), walls [1].getYStart()};
1428               } else {
1429                 joinPoint = new float [] {walls [1].getXEnd(), walls [1].getYEnd()};
1430               }
1431             } else {
1432               if (Point2D.distanceSq(walls [1].getXStart(), walls [1].getYStart(), walls [0].getXEnd(), walls [0].getYEnd())
1433                   < Point2D.distanceSq(walls [1].getXEnd(), walls [1].getYEnd(), walls [0].getXEnd(), walls [0].getYEnd())) {
1434                 joinPoint = new float [] {walls [1].getXStart(), walls [1].getYStart()};
1435               } else {
1436                 joinPoint = new float [] {walls [1].getXEnd(), walls [1].getYEnd()};
1437               }
1438             }
1439           }
1440         }
1441       }
1442       if (joinPoint != null) {
1443         JoinedWall [] joinedWalls = JoinedWall.getJoinedWalls(Arrays.asList(walls [0], walls [1]));
1444         doJoinWalls(joinedWalls, joinPoint);
1445         this.undoSupport.postEdit(new WallsJoiningUndoableEdit(this, this.preferences,
1446             selectedItems.toArray(new Selectable [selectedItems.size()]),
1447             home.isAllLevelsSelection(), joinedWalls, joinPoint));
1448       }
1449     }
1450   }
1451 
1452   /**
1453    * Undoable edit for joining walls.
1454    */
1455   private static class WallsJoiningUndoableEdit extends LocalizedUndoableEdit {
1456     private PlanController      controller;
1457     private final Selectable [] oldSelection;
1458     private final boolean       allLevelsSelection;
1459     private final JoinedWall [] joinedWalls;
1460     private final float []      joinPoint;
1461 
WallsJoiningUndoableEdit(PlanController controller, UserPreferences preferences, Selectable [] oldSelection, boolean allLevelsSelection, JoinedWall [] joinedWalls, float [] joinPoint)1462     public WallsJoiningUndoableEdit(PlanController controller, UserPreferences preferences,
1463                                     Selectable [] oldSelection, boolean allLevelsSelection,
1464                                     JoinedWall [] joinedWalls, float [] joinPoint) {
1465       super(preferences, PlanController.class, "undoJoinWallsName");
1466       this.controller = controller;
1467       this.oldSelection = oldSelection;
1468       this.allLevelsSelection = allLevelsSelection;
1469       this.joinedWalls = joinedWalls;
1470       this.joinPoint = joinPoint;
1471     }
1472 
1473     @Override
undo()1474     public void undo() throws CannotUndoException {
1475       super.undo();
1476       for (JoinedWall joinedWall : this.joinedWalls) {
1477         Wall wall = joinedWall.getWall();
1478         wall.setWallAtStart(joinedWall.getWallAtStart());
1479         if (joinedWall.getWallAtStart() != null) {
1480           if (joinedWall.isJoinedAtEndOfWallAtStart()) {
1481             joinedWall.getWallAtStart().setWallAtEnd(wall);
1482           } else {
1483             joinedWall.getWallAtStart().setWallAtStart(wall);
1484           }
1485         }
1486         wall.setWallAtEnd(joinedWall.getWallAtEnd());
1487         if (joinedWall.getWallAtEnd() != null) {
1488           if (joinedWall.isJoinedAtStartOfWallAtEnd()) {
1489             joinedWall.getWallAtEnd().setWallAtStart(wall);
1490           } else {
1491             joinedWall.getWallAtEnd().setWallAtEnd(wall);
1492           }
1493         }
1494         wall.setXStart(joinedWall.getXStart());
1495         wall.setYStart(joinedWall.getYStart());
1496         wall.setXEnd(joinedWall.getXEnd());
1497         wall.setYEnd(joinedWall.getYEnd());
1498       }
1499       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.allLevelsSelection);
1500     }
1501 
1502     @Override
redo()1503     public void redo() throws CannotRedoException {
1504       super.redo();
1505       this.controller.doJoinWalls(this.joinedWalls, this.joinPoint);
1506     }
1507   }
1508 
1509   /**
1510    * Joins two walls at the given point.
1511    */
doJoinWalls(final JoinedWall [] joinedWalls, float [] joinPoint)1512   private void doJoinWalls(final JoinedWall [] joinedWalls,
1513                            float [] joinPoint) {
1514     Wall [] walls = {joinedWalls [0].getWall(), joinedWalls [1].getWall()};
1515     // Ignore parallel walls
1516     boolean connected = false;
1517     for (int i = 0; i < 2; i++) {
1518       boolean joinAtEnd   = walls [i].getWallAtEnd() == null;
1519       boolean joinAtStart = walls [i].getWallAtStart() == null;
1520       if (joinAtStart && joinAtEnd) {
1521         // Join at the point closest to intersection
1522         if (Point2D.distanceSq(walls [i].getXStart(), walls [i].getYStart(), joinPoint [0], joinPoint [1])
1523             < Point2D.distanceSq(walls [i].getXEnd(), walls [i].getYEnd(), joinPoint [0], joinPoint [1])) {
1524           joinAtEnd = false;
1525         } else {
1526           joinAtStart = false;
1527         }
1528       }
1529       if (joinAtEnd) {
1530         walls [i].setXEnd(joinPoint [0]);
1531         walls [i].setYEnd(joinPoint [1]);
1532       } else if (joinAtStart) {
1533         walls [i].setXStart(joinPoint [0]);
1534         walls [i].setYStart(joinPoint [1]);
1535       }
1536       if (connected
1537           || walls [(i + 1) % 2].getWallAtStart() == null
1538           || walls [(i + 1) % 2].getWallAtEnd() == null) {
1539         if (joinAtEnd) {
1540           walls [i].setWallAtEnd(walls [(i + 1) % 2]);
1541           connected = true;
1542         } else if (joinAtStart) {
1543           walls [i].setWallAtStart(walls [(i + 1) % 2]);
1544           connected = true;
1545         }
1546       }
1547     }
1548     if (connected) {
1549       this.home.setSelectedItems(Arrays.asList(walls [0], walls [1]));
1550     } else {
1551       this.home.setSelectedItems(Arrays.asList(walls [0]));
1552     }
1553   }
1554 
1555   /**
1556    * Controls the direction reverse of selected walls.
1557    */
reverseSelectedWallsDirection()1558   public void reverseSelectedWallsDirection() {
1559     List<Selectable> selectedItems = this.home.getSelectedItems();
1560     List<Wall> selectedWalls = Home.getWallsSubList(selectedItems);
1561     if (!selectedWalls.isEmpty()) {
1562       Wall [] reversedWalls = selectedWalls.toArray(new Wall [selectedWalls.size()]);
1563       doReverseWallsDirection(reversedWalls);
1564       selectAndShowItems(Arrays.asList(reversedWalls), false);
1565       this.undoSupport.postEdit(new WallsDirectionReversingUndoableEdit(this, this.preferences,
1566           selectedItems.toArray(new Selectable [selectedItems.size()]), this.home.isAllLevelsSelection(), reversedWalls));
1567     }
1568   }
1569 
1570   /**
1571    * Undoable edit for reversing walls direction.
1572    */
1573   private static class WallsDirectionReversingUndoableEdit extends LocalizedUndoableEdit {
1574     private final PlanController controller;
1575     private final Selectable []  oldSelection;
1576     private final boolean        allLevelsSelection;
1577     private final Wall []        walls;
1578 
WallsDirectionReversingUndoableEdit(PlanController controller, UserPreferences preferences, Selectable [] oldSelection, boolean allLevelsSelection, Wall [] walls)1579     public WallsDirectionReversingUndoableEdit(PlanController controller, UserPreferences preferences,
1580                                                 Selectable [] oldSelection, boolean allLevelsSelection, Wall [] walls) {
1581       super(preferences, PlanController.class, "undoReverseWallsDirectionName");
1582       this.controller = controller;
1583       this.oldSelection = oldSelection;
1584       this.allLevelsSelection = allLevelsSelection;
1585       this.walls = walls;
1586     }
1587 
1588     @Override
undo()1589     public void undo() throws CannotUndoException {
1590       super.undo();
1591       controller.doReverseWallsDirection(this.walls);
1592       controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.allLevelsSelection);
1593     }
1594 
1595     @Override
redo()1596     public void redo() throws CannotRedoException {
1597       super.redo();
1598       controller.doReverseWallsDirection(this.walls);
1599       controller.selectAndShowItems(Arrays.asList(this.walls), false);
1600     }
1601   }
1602 
1603   /**
1604    * Reverses the <code>walls</code> direction.
1605    */
doReverseWallsDirection(Wall [] walls)1606   private void doReverseWallsDirection(Wall [] walls) {
1607     for (Wall wall : walls) {
1608       float xStart = wall.getXStart();
1609       float yStart = wall.getYStart();
1610       float xEnd = wall.getXEnd();
1611       float yEnd = wall.getYEnd();
1612       wall.setXStart(xEnd);
1613       wall.setYStart(yEnd);
1614       wall.setXEnd(xStart);
1615       wall.setYEnd(yStart);
1616       if (wall.getArcExtent() != null) {
1617         wall.setArcExtent(-wall.getArcExtent());
1618       }
1619 
1620       Wall wallAtStart = wall.getWallAtStart();
1621       boolean joinedAtEndOfWallAtStart =
1622         wallAtStart != null
1623         && wallAtStart.getWallAtEnd() == wall;
1624       boolean joinedAtStartOfWallAtStart =
1625         wallAtStart != null
1626         && wallAtStart.getWallAtStart() == wall;
1627       Wall wallAtEnd = wall.getWallAtEnd();
1628       boolean joinedAtEndOfWallAtEnd =
1629         wallAtEnd != null
1630         && wallAtEnd.getWallAtEnd() == wall;
1631       boolean joinedAtStartOfWallAtEnd =
1632         wallAtEnd != null
1633         && wallAtEnd.getWallAtStart() == wall;
1634 
1635       wall.setWallAtStart(wallAtEnd);
1636       wall.setWallAtEnd(wallAtStart);
1637 
1638       if (joinedAtEndOfWallAtStart) {
1639         wallAtStart.setWallAtEnd(wall);
1640       } else if (joinedAtStartOfWallAtStart) {
1641         wallAtStart.setWallAtStart(wall);
1642       }
1643 
1644       if (joinedAtEndOfWallAtEnd) {
1645         wallAtEnd.setWallAtEnd(wall);
1646       } else if (joinedAtStartOfWallAtEnd) {
1647         wallAtEnd.setWallAtStart(wall);
1648       }
1649 
1650       Float heightAtEnd = wall.getHeightAtEnd();
1651       if (heightAtEnd != null) {
1652         Float height = wall.getHeight();
1653         wall.setHeight(heightAtEnd);
1654         wall.setHeightAtEnd(height);
1655       }
1656 
1657       reverseWallSidesStyle(wall);
1658     }
1659   }
1660 
1661   /**
1662    * Exchanges the style of wall sides.
1663    */
reverseWallSidesStyle(Wall wall)1664   private static void reverseWallSidesStyle(Wall wall) {
1665     Integer rightSideColor = wall.getRightSideColor();
1666     HomeTexture rightSideTexture = wall.getRightSideTexture();
1667     float leftSideShininess = wall.getLeftSideShininess();
1668     Baseboard leftSideBaseboard = wall.getLeftSideBaseboard();
1669     Integer leftSideColor = wall.getLeftSideColor();
1670     HomeTexture leftSideTexture = wall.getLeftSideTexture();
1671     float rightSideShininess = wall.getRightSideShininess();
1672     Baseboard rightSideBaseboard = wall.getRightSideBaseboard();
1673     wall.setLeftSideColor(rightSideColor);
1674     wall.setLeftSideTexture(rightSideTexture);
1675     wall.setLeftSideShininess(rightSideShininess);
1676     wall.setLeftSideBaseboard(rightSideBaseboard);
1677     wall.setRightSideColor(leftSideColor);
1678     wall.setRightSideTexture(leftSideTexture);
1679     wall.setRightSideShininess(leftSideShininess);
1680     wall.setRightSideBaseboard(leftSideBaseboard);
1681   }
1682 
1683   /**
1684    * Controls the split of the selected wall in two joined walls of equal length.
1685    */
splitSelectedWall()1686   public void splitSelectedWall() {
1687     List<Selectable> selectedItems = this.home.getSelectedItems();
1688     List<Wall> selectedWalls = Home.getWallsSubList(selectedItems);
1689     if (selectedWalls.size() == 1) {
1690       boolean allLevelsSelection = this.home.isAllLevelsSelection();
1691       boolean basePlanLocked = this.home.isBasePlanLocked();
1692       Wall splitWall = selectedWalls.get(0);
1693       JoinedWall splitJoinedWall = new JoinedWall(splitWall);
1694       float xStart = splitWall.getXStart();
1695       float yStart = splitWall.getYStart();
1696       float xEnd = splitWall.getXEnd();
1697       float yEnd = splitWall.getYEnd();
1698       float xMiddle = (xStart + xEnd) / 2;
1699       float yMiddle = (yStart + yEnd) / 2;
1700 
1701       Wall wallAtStart = splitWall.getWallAtStart();
1702       boolean joinedAtEndOfWallAtStart =
1703         wallAtStart != null
1704         && wallAtStart.getWallAtEnd() == splitWall;
1705       boolean joinedAtStartOfWallAtStart =
1706         wallAtStart != null
1707         && wallAtStart.getWallAtStart() == splitWall;
1708       Wall wallAtEnd = splitWall.getWallAtEnd();
1709       boolean joinedAtEndOfWallAtEnd =
1710         wallAtEnd != null
1711         && wallAtEnd.getWallAtEnd() == splitWall;
1712       boolean joinedAtStartOfWallAtEnd =
1713         wallAtEnd != null
1714         && wallAtEnd.getWallAtStart() == splitWall;
1715 
1716       // Clone new walls to copy their characteristics
1717       Wall firstWall = (Wall)splitWall.duplicate();
1718       this.home.addWall(firstWall);
1719       firstWall.setLevel(splitWall.getLevel());
1720       Wall secondWall = (Wall)splitWall.duplicate();
1721       this.home.addWall(secondWall);
1722       secondWall.setLevel(splitWall.getLevel());
1723 
1724       // Change split walls end and start point
1725       firstWall.setXEnd(xMiddle);
1726       firstWall.setYEnd(yMiddle);
1727       secondWall.setXStart(xMiddle);
1728       secondWall.setYStart(yMiddle);
1729       if (splitWall.getHeightAtEnd() != null) {
1730         Float heightAtMiddle = (splitWall.getHeight() + splitWall.getHeightAtEnd()) / 2;
1731         firstWall.setHeightAtEnd(heightAtMiddle);
1732         secondWall.setHeight(heightAtMiddle);
1733       }
1734 
1735       firstWall.setWallAtEnd(secondWall);
1736       secondWall.setWallAtStart(firstWall);
1737 
1738       firstWall.setWallAtStart(wallAtStart);
1739       if (joinedAtEndOfWallAtStart) {
1740         wallAtStart.setWallAtEnd(firstWall);
1741       } else if (joinedAtStartOfWallAtStart) {
1742         wallAtStart.setWallAtStart(firstWall);
1743       }
1744 
1745       secondWall.setWallAtEnd(wallAtEnd);
1746       if (joinedAtEndOfWallAtEnd) {
1747         wallAtEnd.setWallAtEnd(secondWall);
1748       } else if (joinedAtStartOfWallAtEnd) {
1749         wallAtEnd.setWallAtStart(secondWall);
1750       }
1751 
1752       // Delete split wall
1753       this.home.deleteWall(splitWall);
1754       selectAndShowItems(Arrays.asList(new Wall [] {firstWall}), false);
1755 
1756       this.undoSupport.postEdit(new WallSplittingUndoableEdit(this, this.preferences,
1757           selectedItems.toArray(new Selectable [selectedItems.size()]), basePlanLocked, allLevelsSelection,
1758           splitJoinedWall, new JoinedWall(firstWall), new JoinedWall(secondWall), this.home.isBasePlanLocked()));
1759     }
1760   }
1761 
1762   /**
1763    * Undoable edit for splitting wall.
1764    */
1765   private static class WallSplittingUndoableEdit extends LocalizedUndoableEdit {
1766     private final PlanController controller;
1767     private final Selectable [] oldSelection;
1768     private final boolean       oldBasePlanLocked;
1769     private final boolean       oldAllLevelsSelection;
1770     private final JoinedWall    splitJoinedWall;
1771     private final JoinedWall    firstJoinedWall;
1772     private final JoinedWall    secondJoinedWall;
1773     private final boolean       newBasePlanLocked;
1774 
WallSplittingUndoableEdit(PlanController controller, UserPreferences preferences, Selectable [] oldSelection, boolean oldBasePlanLocked, boolean oldAllLevelsSelection, JoinedWall splitJoinedWall, JoinedWall firstJoinedWall, JoinedWall secondJoinedWall, boolean newBasePlanLocked)1775     public WallSplittingUndoableEdit(PlanController controller, UserPreferences preferences, Selectable [] oldSelection, boolean oldBasePlanLocked,
1776                                      boolean oldAllLevelsSelection, JoinedWall splitJoinedWall,
1777                                      JoinedWall firstJoinedWall, JoinedWall secondJoinedWall,
1778                                      boolean newBasePlanLocked) {
1779       super(preferences, PlanController.class, "undoSplitWallName");
1780       this.controller = controller;
1781       this.oldSelection = oldSelection;
1782       this.oldBasePlanLocked = oldBasePlanLocked;
1783       this.oldAllLevelsSelection = oldAllLevelsSelection;
1784       this.splitJoinedWall = splitJoinedWall;
1785       this.firstJoinedWall = firstJoinedWall;
1786       this.secondJoinedWall = secondJoinedWall;
1787       this.newBasePlanLocked = newBasePlanLocked;
1788     }
1789 
1790     @Override
undo()1791     public void undo() throws CannotUndoException {
1792       super.undo();
1793       this.controller.doDeleteWalls(new JoinedWall [] {this.firstJoinedWall, this.secondJoinedWall}, this.oldBasePlanLocked);
1794       this.controller.doAddWalls(new JoinedWall [] {this.splitJoinedWall}, this.oldBasePlanLocked);
1795       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.oldAllLevelsSelection);
1796     }
1797 
1798     @Override
redo()1799     public void redo() throws CannotRedoException {
1800       super.redo();
1801       this.controller.doDeleteWalls(new JoinedWall [] {this.splitJoinedWall}, this.newBasePlanLocked);
1802       this.controller.doAddWalls(new JoinedWall [] {this.firstJoinedWall, this.secondJoinedWall}, this.newBasePlanLocked);
1803       this.controller.selectAndShowItems(Arrays.asList(new Wall [] {this.firstJoinedWall.getWall()}), false);
1804     }
1805   }
1806 
1807   /**
1808    * Controls the modification of the selected rooms.
1809    */
modifySelectedRooms()1810   public void modifySelectedRooms() {
1811     if (!Home.getRoomsSubList(this.home.getSelectedItems()).isEmpty()) {
1812       new RoomController(this.home, this.preferences, this.viewFactory,
1813           this.contentManager, this.undoSupport).displayView(getView());
1814     }
1815   }
1816 
1817   /**
1818    * Returns a new label. The new label isn't added to home.
1819    */
createLabel(float x, float y)1820   private void createLabel(float x, float y) {
1821     new LabelController(this.home, x, y, this.preferences, this.viewFactory,
1822         this.undoSupport).displayView(getView());
1823   }
1824 
1825   /**
1826    * Controls the modification of the selected labels.
1827    */
modifySelectedLabels()1828   public void modifySelectedLabels() {
1829     if (!Home.getLabelsSubList(this.home.getSelectedItems()).isEmpty()) {
1830       new LabelController(this.home, this.preferences, this.viewFactory,
1831           this.undoSupport).displayView(getView());
1832     }
1833   }
1834 
1835   /**
1836    * Controls the modification of the selected polylines.
1837    * @since 5.0
1838    */
modifySelectedPolylines()1839   public void modifySelectedPolylines() {
1840     if (!Home.getPolylinesSubList(this.home.getSelectedItems()).isEmpty()) {
1841       new PolylineController(this.home, this.preferences, this.viewFactory,
1842           this.contentManager, this.undoSupport).displayView(getView());
1843     }
1844   }
1845 
1846   /**
1847    * Controls the modification of the compass.
1848    */
modifyCompass()1849   public void modifyCompass() {
1850     new CompassController(this.home, this.preferences, this.viewFactory,
1851         this.undoSupport).displayView(getView());
1852   }
1853 
1854   /**
1855    * Controls the modification of the observer camera.
1856    */
modifyObserverCamera()1857   public void modifyObserverCamera() {
1858     new ObserverCameraController(this.home, this.preferences, this.viewFactory).displayView(getView());
1859   }
1860 
1861   /**
1862    * Toggles bold style of texts in selected items.
1863    */
toggleBoldStyle()1864   public void toggleBoldStyle() {
1865     // Find if selected items are all bold or not
1866     Boolean selectionBoldStyle = null;
1867     for (Selectable item : this.home.getSelectedItems()) {
1868       Boolean bold;
1869       if (item instanceof Label) {
1870         bold = getItemTextStyle(item, ((Label)item).getStyle()).isBold();
1871       } else if (item instanceof HomePieceOfFurniture
1872           && ((HomePieceOfFurniture)item).isVisible()) {
1873         bold = getItemTextStyle(item, ((HomePieceOfFurniture)item).getNameStyle()).isBold();
1874       } else if (item instanceof Room) {
1875         Room room = (Room)item;
1876         bold = getItemTextStyle(room, room.getNameStyle()).isBold();
1877         if (bold != getItemTextStyle(room, room.getAreaStyle()).isBold()) {
1878           bold = null;
1879         }
1880       } else if (item instanceof DimensionLine) {
1881         bold = getItemTextStyle(item, ((DimensionLine)item).getLengthStyle()).isBold();
1882       } else {
1883         continue;
1884       }
1885       if (selectionBoldStyle == null) {
1886         selectionBoldStyle = bold;
1887       } else if (bold == null || !selectionBoldStyle.equals(bold)) {
1888         selectionBoldStyle = null;
1889         break;
1890       }
1891     }
1892 
1893     // Apply new bold style to all selected items
1894     if (selectionBoldStyle == null) {
1895       selectionBoldStyle = Boolean.TRUE;
1896     } else {
1897       selectionBoldStyle = !selectionBoldStyle;
1898     }
1899 
1900     List<Selectable> itemsWithText = new ArrayList<Selectable>();
1901     List<TextStyle> oldTextStyles = new ArrayList<TextStyle>();
1902     List<TextStyle> textStyles = new ArrayList<TextStyle>();
1903     for (Selectable item : this.home.getSelectedItems()) {
1904       if (item instanceof Label) {
1905         Label label = (Label)item;
1906         itemsWithText.add(label);
1907         TextStyle oldTextStyle = getItemTextStyle(label, label.getStyle());
1908         oldTextStyles.add(oldTextStyle);
1909         textStyles.add(oldTextStyle.deriveBoldStyle(selectionBoldStyle));
1910       } else if (item instanceof HomePieceOfFurniture) {
1911         HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
1912         if (piece.isVisible()) {
1913           itemsWithText.add(piece);
1914           TextStyle oldNameStyle = getItemTextStyle(piece, piece.getNameStyle());
1915           oldTextStyles.add(oldNameStyle);
1916           textStyles.add(oldNameStyle.deriveBoldStyle(selectionBoldStyle));
1917         }
1918       } else if (item instanceof Room) {
1919         final Room room = (Room)item;
1920         itemsWithText.add(room);
1921         TextStyle oldNameStyle = getItemTextStyle(room, room.getNameStyle());
1922         oldTextStyles.add(oldNameStyle);
1923         textStyles.add(oldNameStyle.deriveBoldStyle(selectionBoldStyle));
1924         TextStyle oldAreaStyle = getItemTextStyle(room, room.getAreaStyle());
1925         oldTextStyles.add(oldAreaStyle);
1926         textStyles.add(oldAreaStyle.deriveBoldStyle(selectionBoldStyle));
1927       } else if (item instanceof DimensionLine) {
1928         DimensionLine dimensionLine = (DimensionLine)item;
1929         itemsWithText.add(dimensionLine);
1930         TextStyle oldLengthStyle = getItemTextStyle(dimensionLine, dimensionLine.getLengthStyle());
1931         oldTextStyles.add(oldLengthStyle);
1932         textStyles.add(oldLengthStyle.deriveBoldStyle(selectionBoldStyle));
1933       }
1934     }
1935     modifyTextStyle(itemsWithText.toArray(new Selectable [itemsWithText.size()]),
1936         oldTextStyles.toArray(new TextStyle [oldTextStyles.size()]),
1937         textStyles.toArray(new TextStyle [textStyles.size()]));
1938   }
1939 
1940   /**
1941    * Returns <code>textStyle</code> if not null or the default text style.
1942    */
getItemTextStyle(Selectable item, TextStyle textStyle)1943   private TextStyle getItemTextStyle(Selectable item, TextStyle textStyle) {
1944     if (textStyle == null) {
1945       textStyle = this.preferences.getDefaultTextStyle(item.getClass());
1946     }
1947     return textStyle;
1948   }
1949 
1950   /**
1951    * Toggles italic style of texts in selected items.
1952    */
toggleItalicStyle()1953   public void toggleItalicStyle() {
1954     // Find if selected items are all italic or not
1955     Boolean selectionItalicStyle = null;
1956     for (Selectable item : this.home.getSelectedItems()) {
1957       Boolean italic;
1958       if (item instanceof Label) {
1959         italic = getItemTextStyle(item, ((Label)item).getStyle()).isItalic();
1960       } else if (item instanceof HomePieceOfFurniture
1961           && ((HomePieceOfFurniture)item).isVisible()) {
1962         italic = getItemTextStyle(item, ((HomePieceOfFurniture)item).getNameStyle()).isItalic();
1963       } else if (item instanceof Room) {
1964         Room room = (Room)item;
1965         italic = getItemTextStyle(room, room.getNameStyle()).isItalic();
1966         if (italic != getItemTextStyle(room, room.getAreaStyle()).isItalic()) {
1967           italic = null;
1968         }
1969       } else if (item instanceof DimensionLine) {
1970         italic = getItemTextStyle(item, ((DimensionLine)item).getLengthStyle()).isItalic();
1971       } else {
1972         continue;
1973       }
1974       if (selectionItalicStyle == null) {
1975         selectionItalicStyle = italic;
1976       } else if (italic == null || !selectionItalicStyle.equals(italic)) {
1977         selectionItalicStyle = null;
1978         break;
1979       }
1980     }
1981 
1982     // Apply new italic style to all selected items
1983     if (selectionItalicStyle == null) {
1984       selectionItalicStyle = Boolean.TRUE;
1985     } else {
1986       selectionItalicStyle = !selectionItalicStyle;
1987     }
1988 
1989     List<Selectable> itemsWithText = new ArrayList<Selectable>();
1990     List<TextStyle> oldTextStyles = new ArrayList<TextStyle>();
1991     List<TextStyle> textStyles = new ArrayList<TextStyle>();
1992     for (Selectable item : this.home.getSelectedItems()) {
1993       if (item instanceof Label) {
1994         Label label = (Label)item;
1995         itemsWithText.add(label);
1996         TextStyle oldTextStyle = getItemTextStyle(label, label.getStyle());
1997         oldTextStyles.add(oldTextStyle);
1998         textStyles.add(oldTextStyle.deriveItalicStyle(selectionItalicStyle));
1999       } else if (item instanceof HomePieceOfFurniture) {
2000         HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
2001         if (piece.isVisible()) {
2002           itemsWithText.add(piece);
2003           TextStyle oldNameStyle = getItemTextStyle(piece, piece.getNameStyle());
2004           oldTextStyles.add(oldNameStyle);
2005           textStyles.add(oldNameStyle.deriveItalicStyle(selectionItalicStyle));
2006         }
2007       } else if (item instanceof Room) {
2008         final Room room = (Room)item;
2009         itemsWithText.add(room);
2010         TextStyle oldNameStyle = getItemTextStyle(room, room.getNameStyle());
2011         oldTextStyles.add(oldNameStyle);
2012         textStyles.add(oldNameStyle.deriveItalicStyle(selectionItalicStyle));
2013         TextStyle oldAreaStyle = getItemTextStyle(room, room.getAreaStyle());
2014         oldTextStyles.add(oldAreaStyle);
2015         textStyles.add(oldAreaStyle.deriveItalicStyle(selectionItalicStyle));
2016       } else if (item instanceof DimensionLine) {
2017         DimensionLine dimensionLine = (DimensionLine)item;
2018         itemsWithText.add(dimensionLine);
2019         TextStyle oldLengthStyle = getItemTextStyle(dimensionLine, dimensionLine.getLengthStyle());
2020         oldTextStyles.add(oldLengthStyle);
2021         textStyles.add(oldLengthStyle.deriveItalicStyle(selectionItalicStyle));
2022       }
2023     }
2024     modifyTextStyle(itemsWithText.toArray(new Selectable [itemsWithText.size()]),
2025         oldTextStyles.toArray(new TextStyle [oldTextStyles.size()]),
2026         textStyles.toArray(new TextStyle [textStyles.size()]));
2027   }
2028 
2029   /**
2030    * Increase the size of texts in selected items.
2031    */
increaseTextSize()2032   public void increaseTextSize() {
2033     applyFactorToTextSize(1.1f);
2034   }
2035 
2036   /**
2037    * Decrease the size of texts in selected items.
2038    */
decreaseTextSize()2039   public void decreaseTextSize() {
2040     applyFactorToTextSize(1 / 1.1f);
2041   }
2042 
2043   /**
2044    * Applies a factor to the font size of the texts of the selected items in home.
2045    */
applyFactorToTextSize(float factor)2046   private void applyFactorToTextSize(float factor) {
2047     List<Selectable> itemsWithText = new ArrayList<Selectable>();
2048     List<TextStyle> oldTextStyles = new ArrayList<TextStyle>();
2049     List<TextStyle> textStyles = new ArrayList<TextStyle>();
2050     for (Selectable item : this.home.getSelectedItems()) {
2051       if (item instanceof Label) {
2052         Label label = (Label)item;
2053         itemsWithText.add(label);
2054         TextStyle oldLabelStyle = getItemTextStyle(item, label.getStyle());
2055         oldTextStyles.add(oldLabelStyle);
2056         textStyles.add(oldLabelStyle.deriveStyle(Math.round(oldLabelStyle.getFontSize() * factor)));
2057       } else if (item instanceof HomePieceOfFurniture) {
2058         HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
2059         if (piece.isVisible()) {
2060           itemsWithText.add(piece);
2061           TextStyle oldNameStyle = getItemTextStyle(piece, piece.getNameStyle());
2062           oldTextStyles.add(oldNameStyle);
2063           textStyles.add(oldNameStyle.deriveStyle(Math.round(oldNameStyle.getFontSize() * factor)));
2064         }
2065       } else if (item instanceof Room) {
2066         final Room room = (Room)item;
2067         itemsWithText.add(room);
2068         TextStyle oldNameStyle = getItemTextStyle(room, room.getNameStyle());
2069         oldTextStyles.add(oldNameStyle);
2070         textStyles.add(oldNameStyle.deriveStyle(Math.round(oldNameStyle.getFontSize() * factor)));
2071         TextStyle oldAreaStyle = getItemTextStyle(room, room.getAreaStyle());
2072         oldTextStyles.add(oldAreaStyle);
2073         textStyles.add(oldAreaStyle.deriveStyle(Math.round(oldAreaStyle.getFontSize() * factor)));
2074       } else if (item instanceof DimensionLine) {
2075         DimensionLine dimensionLine = (DimensionLine)item;
2076         itemsWithText.add(dimensionLine);
2077         TextStyle oldLengthStyle = getItemTextStyle(dimensionLine, dimensionLine.getLengthStyle());
2078         oldTextStyles.add(oldLengthStyle);
2079         textStyles.add(oldLengthStyle.deriveStyle(Math.round(oldLengthStyle.getFontSize() * factor)));
2080       }
2081     }
2082     modifyTextStyle(itemsWithText.toArray(new Selectable [itemsWithText.size()]),
2083         oldTextStyles.toArray(new TextStyle [oldTextStyles.size()]),
2084         textStyles.toArray(new TextStyle [textStyles.size()]));
2085   }
2086 
2087   /**
2088    * Changes the style of items and posts an undoable change style operation.
2089    */
modifyTextStyle(final Selectable [] items, final TextStyle [] oldStyles, final TextStyle [] styles)2090   private void modifyTextStyle(final Selectable [] items,
2091                                final TextStyle [] oldStyles,
2092                                final TextStyle [] styles) {
2093     final boolean allLevelsSelection = home.isAllLevelsSelection();
2094     List<Selectable> oldSelectedItems = this.home.getSelectedItems();
2095     final Selectable [] oldSelection =
2096         oldSelectedItems.toArray(new Selectable [oldSelectedItems.size()]);
2097 
2098     doModifyTextStyle(items, styles);
2099     this.undoSupport.postEdit(new TextStyleModificationUndoableEdit(this, this.preferences,
2100         oldSelection, allLevelsSelection, oldStyles, items, styles));
2101   }
2102 
2103   /**
2104    * Undoable edit for text style modification.
2105    */
2106   private static class TextStyleModificationUndoableEdit extends LocalizedUndoableEdit {
2107     private final PlanController controller;
2108     private final Selectable []  oldSelection;
2109     private final boolean        allLevelsSelection;
2110     private final TextStyle []   oldStyles;
2111     private final Selectable []  items;
2112     private final TextStyle []   styles;
2113 
TextStyleModificationUndoableEdit(PlanController controller, UserPreferences preferences, Selectable [] oldSelection, boolean allLevelsSelection, TextStyle [] oldStyles, Selectable [] items, TextStyle [] styles)2114     public TextStyleModificationUndoableEdit(PlanController controller, UserPreferences preferences,
2115                                              Selectable [] oldSelection, boolean allLevelsSelection, TextStyle [] oldStyles,
2116                                              Selectable [] items, TextStyle [] styles) {
2117       super(preferences, PlanController.class, "undoModifyTextStyleName");
2118       this.controller = controller;
2119       this.oldSelection = oldSelection;
2120       this.allLevelsSelection = allLevelsSelection;
2121       this.oldStyles = oldStyles;
2122       this.items = items;
2123       this.styles = styles;
2124     }
2125 
2126     @Override
undo()2127     public void undo() throws CannotUndoException {
2128       super.undo();
2129       doModifyTextStyle(this.items, this.oldStyles);
2130       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.allLevelsSelection);
2131     }
2132 
2133     @Override
redo()2134     public void redo() throws CannotRedoException {
2135       super.redo();
2136       doModifyTextStyle(this.items, this.styles);
2137       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.allLevelsSelection);
2138     }
2139   }
2140 
2141   /**
2142    * Changes the style of items.
2143    */
doModifyTextStyle(Selectable [] items, TextStyle [] styles)2144   private static void doModifyTextStyle(Selectable [] items, TextStyle [] styles) {
2145     int styleIndex = 0;
2146     for (Selectable item : items) {
2147       if (item instanceof Label) {
2148         ((Label)item).setStyle(styles [styleIndex++]);
2149       } else if (item instanceof HomePieceOfFurniture) {
2150         HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
2151         if (piece.isVisible()) {
2152           piece.setNameStyle(styles [styleIndex++]);
2153         }
2154       } else if (item instanceof Room) {
2155         final Room room = (Room)item;
2156         room.setNameStyle(styles [styleIndex++]);
2157         room.setAreaStyle(styles [styleIndex++]);
2158       } else if (item instanceof DimensionLine) {
2159         ((DimensionLine)item).setLengthStyle(styles [styleIndex++]);
2160       }
2161     }
2162   }
2163 
2164   /**
2165    * Returns the minimum scale of the plan view.
2166    */
getMinimumScale()2167   public float getMinimumScale() {
2168     return 0.01f;
2169   }
2170 
2171   /**
2172    * Returns the maximum scale of the plan view.
2173    */
getMaximumScale()2174   public float getMaximumScale() {
2175     return 10f;
2176   }
2177 
2178   /**
2179    * Returns the scale in plan view.
2180    */
getScale()2181   public float getScale() {
2182     return getView().getScale();
2183   }
2184 
2185   /**
2186    * Controls the scale in plan view and and fires a <code>PropertyChangeEvent</code>.
2187    */
setScale(float scale)2188   public void setScale(float scale) {
2189     scale = Math.max(getMinimumScale(), Math.min(scale, getMaximumScale()));
2190     if (scale != getView().getScale()) {
2191       float oldScale = getView().getScale();
2192       this.furnitureSidesCache.clear();
2193       if (getView() != null) {
2194         int x = getView().convertXModelToScreen(getXLastMouseMove());
2195         int y = getView().convertXModelToScreen(getYLastMouseMove());
2196         getView().setScale(scale);
2197         // Update mouse location
2198         moveMouse(getView().convertXPixelToModel(x), getView().convertYPixelToModel(y));
2199       }
2200       this.propertyChangeSupport.firePropertyChange(Property.SCALE.name(), oldScale, scale);
2201       this.home.setProperty(SCALE_VISUAL_PROPERTY, String.valueOf(scale));
2202     }
2203   }
2204 
2205   /**
2206    * Sets the selected level in home.
2207    */
setSelectedLevel(Level level)2208   public final void setSelectedLevel(Level level) {
2209     this.home.setSelectedLevel(level);
2210   }
2211 
2212   /**
2213    * Selects all visible items in the selected level of home.
2214    */
2215   @Override
selectAll()2216   public void selectAll() {
2217     List<Selectable> all = getVisibleItemsAtSelectedLevel();
2218     if (this.home.isBasePlanLocked()) {
2219       this.home.setSelectedItems(getItemsNotPartOfBasePlan(all));
2220     } else {
2221       this.home.setSelectedItems(all);
2222     }
2223     this.home.setAllLevelsSelection(false);
2224   }
2225 
2226   /**
2227    * Returns the viewable and selectable home items at the selected level, except camera.
2228    */
getVisibleItemsAtSelectedLevel()2229   private List<Selectable> getVisibleItemsAtSelectedLevel() {
2230     List<Selectable> selectableItems = new ArrayList<Selectable>();
2231     Level selectedLevel = this.home.getSelectedLevel();
2232     for (Selectable item : this.home.getSelectableViewableItems()) {
2233       if (item instanceof HomePieceOfFurniture) {
2234         if (isPieceOfFurnitureVisibleAtSelectedLevel((HomePieceOfFurniture)item)) {
2235           selectableItems.add(item);
2236         }
2237       } else if (!(item instanceof Elevatable)
2238           || ((Elevatable)item).isAtLevel(selectedLevel)) {
2239         selectableItems.add(item);
2240       }
2241     }
2242     return selectableItems;
2243   }
2244 
2245   /**
2246    * Selects all visible items in all levels of home.
2247    * @since 4.4
2248    */
selectAllAtAllLevels()2249   public void selectAllAtAllLevels() {
2250     List<Selectable> allItems = new ArrayList<Selectable>(this.home.getSelectableViewableItems());
2251     if (this.home.isBasePlanLocked()) {
2252       allItems = getItemsNotPartOfBasePlan(allItems);
2253     }
2254     this.home.setSelectedItems(allItems);
2255     this.home.setAllLevelsSelection(true);
2256   }
2257 
2258   /**
2259    * Returns the visible (fully or partially) rooms at the selected level in home.
2260    */
getDetectableRoomsAtSelectedLevel()2261   private List<Room> getDetectableRoomsAtSelectedLevel() {
2262     List<Room> rooms = this.home.getRooms();
2263     Level selectedLevel = this.home.getSelectedLevel();
2264     List<Level> levels = this.home.getLevels();
2265     if (selectedLevel == null || levels.size() <= 1) {
2266       return rooms;
2267     } else {
2268       List<Room> visibleRooms = new ArrayList<Room>(rooms.size());
2269       int selectedLevelIndex = levels.indexOf(selectedLevel);
2270       boolean level0 = levels.get(0) == selectedLevel
2271           || levels.get(selectedLevelIndex - 1).getElevation() == selectedLevel.getElevation();
2272       Level otherLevel = levels.get(level0 && selectedLevelIndex < levels.size() - 1
2273           ? selectedLevelIndex + 1
2274           : selectedLevelIndex  - 1);
2275       for (Room room : rooms) {
2276         if (room.isAtLevel(selectedLevel)
2277             || otherLevel != null
2278                 && room.isAtLevel(otherLevel)
2279                 && (level0 && room.isFloorVisible()
2280                     || !level0 && room.isCeilingVisible())) {
2281           visibleRooms.add(room);
2282         }
2283       }
2284       return visibleRooms;
2285     }
2286   }
2287 
2288   /**
2289    * Returns the visible (fully or partially) walls at the selected level in home.
2290    */
getDetectableWallsAtSelectedLevel()2291   private Collection<Wall> getDetectableWallsAtSelectedLevel() {
2292     Collection<Wall> walls = this.home.getWalls();
2293     Level selectedLevel = this.home.getSelectedLevel();
2294     List<Level> levels = this.home.getLevels();
2295     if (selectedLevel == null || levels.size() <= 1) {
2296       return walls;
2297     } else {
2298       Collection<Wall> visibleWalls = new ArrayList<Wall>(walls.size());
2299       int selectedLevelIndex = levels.indexOf(selectedLevel);
2300       boolean level0 = levels.get(0) == selectedLevel
2301           || levels.get(selectedLevelIndex - 1).getElevation() == selectedLevel.getElevation();
2302       Level otherLevel = levels.get(level0 && selectedLevelIndex < levels.size() - 1
2303           ? selectedLevelIndex + 1
2304           : selectedLevelIndex  - 1);
2305       for (Wall wall : walls) {
2306         if (wall.isAtLevel(selectedLevel)
2307             || otherLevel != null
2308                && wall.isAtLevel(otherLevel)) {
2309           visibleWalls.add(wall);
2310         }
2311       }
2312       return visibleWalls;
2313     }
2314   }
2315 
2316   /**
2317    * Returns the horizontal ruler of the plan view.
2318    */
getHorizontalRulerView()2319   public View getHorizontalRulerView() {
2320     return getView().getHorizontalRuler();
2321   }
2322 
2323   /**
2324    * Returns the vertical ruler of the plan view.
2325    */
getVerticalRulerView()2326   public View getVerticalRulerView() {
2327     return getView().getVerticalRuler();
2328   }
2329 
addModelListeners()2330   private void addModelListeners() {
2331     this.selectionListener = new SelectionListener() {
2332         public void selectionChanged(SelectionEvent ev) {
2333           selectLevelFromSelectedItems();
2334           if (getView() != null) {
2335             getView().makeSelectionVisible();
2336           }
2337         }
2338       };
2339     this.home.addSelectionListener(this.selectionListener);
2340     // Ensure observer camera is visible when its size, location or angles change
2341     this.home.getObserverCamera().addPropertyChangeListener(new PropertyChangeListener() {
2342         public void propertyChange(PropertyChangeEvent ev) {
2343           if (home.getSelectedItems().contains(ev.getSource())) {
2344             if (getView() != null) {
2345               getView().makeSelectionVisible();
2346             }
2347           }
2348         }
2349       });
2350     this.wallChangeListener = new PropertyChangeListener() {
2351         public void propertyChange(PropertyChangeEvent ev) {
2352           String propertyName = ev.getPropertyName();
2353           if (Wall.Property.X_START.name().equals(propertyName)
2354               || Wall.Property.X_END.name().equals(propertyName)
2355               || Wall.Property.Y_START.name().equals(propertyName)
2356               || Wall.Property.Y_END.name().equals(propertyName)
2357               || Wall.Property.WALL_AT_START.name().equals(propertyName)
2358               || Wall.Property.WALL_AT_END.name().equals(propertyName)
2359               || Wall.Property.THICKNESS.name().equals(propertyName)
2360               || Wall.Property.ARC_EXTENT.name().equals(propertyName)
2361               || Wall.Property.LEVEL.name().equals(propertyName)
2362               || Wall.Property.HEIGHT.name().equals(propertyName)
2363               || Wall.Property.HEIGHT_AT_END.name().equals(propertyName)
2364               || Wall.Property.LEFT_SIDE_BASEBOARD.name().equals(propertyName)
2365               || Wall.Property.RIGHT_SIDE_BASEBOARD.name().equals(propertyName)) {
2366             resetAreaCache();
2367             // Unselect unreachable wall
2368             Wall wall = (Wall)ev.getSource();
2369             if (!wall.isAtLevel(home.getSelectedLevel())) {
2370               List<Selectable> selectedItems = new ArrayList<Selectable>(home.getSelectedItems());
2371               if (selectedItems.remove(wall)) {
2372                 selectItems(selectedItems, home.isAllLevelsSelection());
2373               }
2374             }
2375           }
2376         }
2377       };
2378     for (Wall wall : this.home.getWalls()) {
2379       wall.addPropertyChangeListener(this.wallChangeListener);
2380     }
2381     this.home.addWallsListener(new CollectionListener<Wall> () {
2382         public void collectionChanged(CollectionEvent<Wall> ev) {
2383           if (ev.getType() == CollectionEvent.Type.ADD) {
2384             ev.getItem().addPropertyChangeListener(wallChangeListener);
2385           } else if (ev.getType() == CollectionEvent.Type.DELETE) {
2386             ev.getItem().removePropertyChangeListener(wallChangeListener);
2387           }
2388           resetAreaCache();
2389         }
2390       });
2391     // Add listener to update furnitureBordersCache when walls change
2392     final PropertyChangeListener furnitureChangeListener = new PropertyChangeListener() {
2393         public void propertyChange(PropertyChangeEvent ev) {
2394           String propertyName = ev.getPropertyName();
2395           if (HomePieceOfFurniture.Property.X.name().equals(propertyName)
2396               || HomePieceOfFurniture.Property.Y.name().equals(propertyName)
2397               || HomePieceOfFurniture.Property.WIDTH_IN_PLAN.name().equals(propertyName)
2398               || HomePieceOfFurniture.Property.DEPTH_IN_PLAN.name().equals(propertyName)) {
2399             furnitureSidesCache.remove((HomePieceOfFurniture)ev.getSource());
2400           }
2401         }
2402       };
2403     this.furnitureSizeChangeListener = new PropertyChangeListener() {
2404         public void propertyChange(PropertyChangeEvent ev) {
2405           HomePieceOfFurniture piece = (HomePieceOfFurniture)ev.getSource();
2406           String propertyName = ev.getPropertyName();
2407           if (HomePieceOfFurniture.Property.MODEL.name().equals(propertyName)
2408               || HomePieceOfFurniture.Property.MODEL_MIRRORED.name().equals(propertyName)
2409               || HomePieceOfFurniture.Property.MODEL_ROTATION.name().equals(propertyName)
2410               || HomePieceOfFurniture.Property.WIDTH.name().equals(propertyName)
2411               || HomePieceOfFurniture.Property.DEPTH.name().equals(propertyName)
2412               || HomePieceOfFurniture.Property.HEIGHT.name().equals(propertyName)
2413               || HomePieceOfFurniture.Property.ROLL.name().equals(propertyName)
2414               || HomePieceOfFurniture.Property.PITCH.name().equals(propertyName)) {
2415             // Update piece size in plan
2416             float [] size = getView().getPieceOfFurnitureSizeInPlan(piece);
2417             if (size != null) {
2418               piece.setWidthInPlan(size [0]);
2419               piece.setDepthInPlan(size [1]);
2420               piece.setHeightInPlan(size [2]);
2421             } else if (HomePieceOfFurniture.Property.WIDTH.name().equals(propertyName)) {
2422               // If the 2D view is unable to send the new piece size in the plan
2423               // the size change is considered to be applied proportionally
2424               float scale = piece.getWidth() / ((Number)ev.getOldValue()).floatValue();
2425               piece.setWidthInPlan(scale * piece.getWidthInPlan());
2426               piece.setDepthInPlan(scale * piece.getDepthInPlan());
2427               piece.setHeightInPlan(scale * piece.getHeightInPlan());
2428             }
2429           }
2430         }
2431       };
2432     for (HomePieceOfFurniture piece : this.home.getFurniture()) {
2433       piece.addPropertyChangeListener(furnitureChangeListener);
2434       piece.addPropertyChangeListener(furnitureSizeChangeListener);
2435       if (piece instanceof HomeFurnitureGroup) {
2436         for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
2437           childPiece.addPropertyChangeListener(furnitureSizeChangeListener);
2438         }
2439       }
2440     }
2441     this.home.addFurnitureListener(new CollectionListener<HomePieceOfFurniture> () {
2442         public void collectionChanged(CollectionEvent<HomePieceOfFurniture> ev) {
2443           HomePieceOfFurniture piece = ev.getItem();
2444           if (ev.getType() == CollectionEvent.Type.ADD) {
2445             piece.addPropertyChangeListener(furnitureChangeListener);
2446             piece.addPropertyChangeListener(furnitureSizeChangeListener);
2447             if (piece instanceof HomeFurnitureGroup) {
2448               for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
2449                 childPiece.addPropertyChangeListener(furnitureSizeChangeListener);
2450               }
2451             }
2452           } else if (ev.getType() == CollectionEvent.Type.DELETE) {
2453             piece.removePropertyChangeListener(furnitureChangeListener);
2454             furnitureSidesCache.remove(piece);
2455             piece.removePropertyChangeListener(furnitureSizeChangeListener);
2456             if (piece instanceof HomeFurnitureGroup) {
2457               for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
2458                 childPiece.removePropertyChangeListener(furnitureSizeChangeListener);
2459               }
2460             }
2461           }
2462         }
2463       });
2464 
2465     this.home.addPropertyChangeListener(Home.Property.SELECTED_LEVEL, new PropertyChangeListener() {
2466         public void propertyChange(PropertyChangeEvent ev) {
2467           resetAreaCache();
2468         }
2469       });
2470     this.home.getObserverCamera().setFixedSize(home.getLevels().size() >= 2);
2471     this.home.addLevelsListener(new CollectionListener<Level>() {
2472         public void collectionChanged(CollectionEvent<Level> ev) {
2473           home.getObserverCamera().setFixedSize(home.getLevels().size() >= 2);
2474         }
2475       });
2476   }
2477 
2478   /**
2479    * Returns the selection listener add to the controlled home.
2480    */
getSelectionListener()2481   private SelectionListener getSelectionListener() {
2482     return this.selectionListener;
2483   }
2484 
resetAreaCache()2485   private void resetAreaCache() {
2486     wallsAreaCache = null;
2487     wallsIncludingBaseboardsAreaCache = null;
2488     insideWallsAreaCache = null;
2489     roomPathsCache = null;
2490   }
2491 
2492   /**
2493    * Displays in plan view the feedback of <code>draggedItems</code>,
2494    * during a drag and drop operation initiated from outside of plan view.
2495    */
startDraggedItems(List<Selectable> draggedItems, float x, float y)2496   public void startDraggedItems(List<Selectable> draggedItems, float x, float y) {
2497     this.draggedItems = draggedItems;
2498     // If magnetism is enabled, adjust furniture size and elevation
2499     if (this.preferences.isMagnetismEnabled()) {
2500       for (HomePieceOfFurniture piece : Home.getFurnitureSubList(draggedItems)) {
2501         if (piece.isResizable()) {
2502           // Roll and pitch angles of dragged items is always 0
2503           piece.setWidth(this.preferences.getLengthUnit().getMagnetizedLength(piece.getWidth(), 0.1f));
2504           piece.setWidthInPlan(piece.getWidth());
2505           piece.setDepth(this.preferences.getLengthUnit().getMagnetizedLength(piece.getDepth(), 0.1f));
2506           piece.setDepthInPlan(piece.getDepth());
2507           piece.setHeight(this.preferences.getLengthUnit().getMagnetizedLength(piece.getHeight(), 0.1f));
2508           piece.setHeightInPlan(piece.getHeight());
2509         }
2510         piece.setElevation(this.preferences.getLengthUnit().getMagnetizedLength(piece.getElevation(), 0.1f));
2511       }
2512     }
2513     if (isModificationState()) {
2514       escape();
2515     }
2516     setState(getDragAndDropState());
2517     moveMouse(x, y);
2518   }
2519 
2520   /**
2521    * Deletes in plan view the feedback of the dragged items.
2522    */
stopDraggedItems()2523   public void stopDraggedItems() {
2524     if (this.state != getDragAndDropState()) {
2525       throw new IllegalStateException("Controller isn't in a drag and drop state");
2526     }
2527     this.draggedItems = null;
2528     setState(this.previousState);
2529   }
2530 
2531   /**
2532    * Attempts to modify <code>piece</code> location depending of its context.
2533    * If the <code>piece</code> is a door or a window and the point (<code>x</code>, <code>y</code>)
2534    * belongs to a wall, the piece will be resized, rotated and moved so
2535    * its opening depth is equal to wall thickness and its angle matches wall direction.
2536    * If the <code>piece</code> isn't a door or a window and the point (<code>x</code>, <code>y</code>)
2537    * belongs to a wall, the piece will be rotated and moved so
2538    * its back face lies along the closest wall side and its angle matches wall direction.
2539    * If the <code>piece</code> isn't a door or a window, its bounding box is included in
2540    * the one of an other object and its elevation is equal to zero, it will be elevated
2541    * to appear on the top of the latter.
2542    */
adjustMagnetizedPieceOfFurniture(HomePieceOfFurniture piece, float x, float y)2543   protected void adjustMagnetizedPieceOfFurniture(HomePieceOfFurniture piece, float x, float y) {
2544     boolean pieceElevationAdjusted = adjustPieceOfFurnitureElevation(piece) != null;
2545     Wall magnetWall = adjustPieceOfFurnitureOnWallAt(piece, x, y, true);
2546     if (!pieceElevationAdjusted) {
2547       adjustPieceOfFurnitureSideBySideAt(piece, magnetWall == null, magnetWall);
2548     }
2549   }
2550 
2551   /**
2552    * Attempts to move and resize <code>piece</code> depending on the wall under the
2553    * point (<code>x</code>, <code>y</code>) and returns that wall it it exists.
2554    * @see #adjustMagnetizedPieceOfFurniture(HomePieceOfFurniture, float, float)
2555    */
adjustPieceOfFurnitureOnWallAt(HomePieceOfFurniture piece, float x, float y, boolean forceOrientation)2556   private Wall adjustPieceOfFurnitureOnWallAt(HomePieceOfFurniture piece,
2557                                               float x, float y, boolean forceOrientation) {
2558     float margin = PIXEL_MARGIN / getScale();
2559     Level selectedLevel = this.home.getSelectedLevel();
2560     float [][] piecePoints = piece.getPoints();
2561 
2562     final boolean includeBaseboards = !piece.isDoorOrWindow()
2563         && piece.getElevation() == 0;
2564     Area wallsArea = getWallsArea(includeBaseboards);
2565     Collection<Wall> walls = this.home.getWalls();
2566 
2567     Wall referenceWall = null;
2568     Float referenceWallArcExtent = null;
2569     if (forceOrientation
2570         || !piece.isDoorOrWindow()) {
2571       // Search if point (x, y) is contained in home walls with no margin
2572       for (Wall wall : walls) {
2573         if (wall.isAtLevel(selectedLevel)
2574             && isLevelNullOrViewable(wall.getLevel())
2575             && wall.containsPoint(x, y, includeBaseboards, 0)
2576             && wall.getStartPointToEndPointDistance() > 0) {
2577           referenceWall = getReferenceWall(wall, x, y);
2578           referenceWallArcExtent = wall.getArcExtent();
2579           break;
2580         }
2581       }
2582       if (referenceWall == null) {
2583         // If not found search if point (x, y) is contained in home walls with a margin
2584         for (Wall wall : walls) {
2585           if (wall.isAtLevel(selectedLevel)
2586               && isLevelNullOrViewable(wall.getLevel())
2587               && wall.containsPoint(x, y, includeBaseboards, 0)
2588               && wall.getStartPointToEndPointDistance() > 0) {
2589             referenceWall = getReferenceWall(wall, x, y);
2590             referenceWallArcExtent = wall.getArcExtent();
2591             break;
2592           }
2593         }
2594       }
2595     }
2596 
2597     if (referenceWall == null) {
2598       // Search if the border of a wall at floor level intersects with the given piece
2599       Area pieceAreaWithMargin = new Area(getRotatedRectangle(
2600           piece.getX() - piece.getWidthInPlan() / 2 - margin, piece.getY() - piece.getDepthInPlan() / 2 - margin,
2601           piece.getWidthInPlan() + 2 * margin, piece.getDepthInPlan() + 2 * margin, piece.getAngle()));
2602       float intersectionWithReferenceWallSurface = 0;
2603       for (Wall wall : walls) {
2604         if (wall.isAtLevel(selectedLevel)
2605             && isLevelNullOrViewable(wall.getLevel())
2606             && wall.getStartPointToEndPointDistance() > 0) {
2607           float [][] wallPoints = wall.getPoints(includeBaseboards);
2608           Area wallAreaIntersection = new Area(getPath(wallPoints));
2609           wallAreaIntersection.intersect(pieceAreaWithMargin);
2610           if (!wallAreaIntersection.isEmpty()) {
2611             float surface = getArea(wallAreaIntersection);
2612             if (surface > intersectionWithReferenceWallSurface) {
2613               intersectionWithReferenceWallSurface = surface;
2614               if (forceOrientation) {
2615                 referenceWall = getReferenceWall(wall, x, y);
2616                 referenceWallArcExtent = wall.getArcExtent();
2617               } else {
2618                 Rectangle2D intersectionBounds = wallAreaIntersection.getBounds2D();
2619                 referenceWall = getReferenceWall(wall, (float)intersectionBounds.getCenterX(), (float)intersectionBounds.getCenterY());
2620                 referenceWallArcExtent = wall.getArcExtent();
2621               }
2622             }
2623           }
2624         }
2625       }
2626     }
2627 
2628     if (referenceWall != null) {
2629       float xPiece = x;
2630       float yPiece = y;
2631       float pieceAngle = piece.getAngle();
2632       float halfWidth = piece.getWidthInPlan() / 2;
2633       float halfDepth = piece.getDepthInPlan() / 2;
2634       double wallAngle = Math.atan2(referenceWall.getYEnd() - referenceWall.getYStart(),
2635           referenceWall.getXEnd() - referenceWall.getXStart());
2636       float [][] wallPoints = referenceWall.getPoints(includeBaseboards);
2637       boolean magnetizedAtRight = wallAngle > -Math.PI / 2 && wallAngle <= Math.PI / 2;
2638       double cosWallAngle = Math.cos(wallAngle);
2639       double sinWallAngle = Math.sin(wallAngle);
2640       double distanceToLeftSide = wallPoints [0][0] != wallPoints [0][1] || wallPoints [1][0] != wallPoints [1][1]
2641           ? Line2D.ptLineDist(wallPoints [0][0], wallPoints [0][1], wallPoints [1][0], wallPoints [1][1], x, y)
2642           : Point2D.distance(wallPoints [0][0], wallPoints [0][1], x, y);
2643       double distanceToRightSide = wallPoints [2][0] != wallPoints [2][1] || wallPoints [3][0] != wallPoints [3][1]
2644           ? Line2D.ptLineDist(wallPoints [2][0], wallPoints [2][1], wallPoints [3][0], wallPoints [3][1], x, y)
2645           : Point2D.distance(wallPoints [2][0], wallPoints [2][1], x, y);
2646       boolean adjustOrientation = forceOrientation
2647           || piece.isDoorOrWindow()
2648           || referenceWall.containsPoint(x, y, includeBaseboards, margin);
2649       if (adjustOrientation) {
2650         double distanceToPieceLeftSide = Line2D.ptLineDist(
2651             piecePoints [0][0], piecePoints [0][1], piecePoints [3][0], piecePoints [3][1], x, y);
2652         double distanceToPieceRightSide = Line2D.ptLineDist(
2653             piecePoints [1][0], piecePoints [1][1], piecePoints [2][0], piecePoints [2][1], x, y);
2654         double distanceToPieceSide = pieceAngle > (3 * Math.PI / 2 + 1E-6) || pieceAngle < (Math.PI / 2 + 1E-6)
2655             ? distanceToPieceLeftSide
2656             : distanceToPieceRightSide;
2657         pieceAngle = (float)(distanceToRightSide < distanceToLeftSide
2658             ? wallAngle
2659             : wallAngle + Math.PI);
2660 
2661         if (piece.isDoorOrWindow()) {
2662           final float thicknessEpsilon = 0.00075f;
2663           float wallDistance;
2664           if (referenceWallArcExtent == null
2665               || referenceWallArcExtent.floatValue() == 0) {
2666             wallDistance = thicknessEpsilon / 2;
2667             if (piece instanceof HomeDoorOrWindow) {
2668               HomeDoorOrWindow doorOrWindow = (HomeDoorOrWindow)piece;
2669               if (piece.isResizable()
2670                   && isItemResizable(piece)
2671                   && doorOrWindow.isWidthDepthDeformable()
2672                   && doorOrWindow.getModelTransformations() == null) {
2673                 // Doors and windows can't be rotated around horizontal axes
2674                 piece.setDepth(thicknessEpsilon
2675                     + referenceWall.getThickness() / doorOrWindow.getWallThickness());
2676                 // Need to set depth in plan because piece isn't added to home during initial drop
2677                 piece.setDepthInPlan(piece.getDepth());
2678                 halfDepth = piece.getDepth() / 2;
2679                 wallDistance += piece.getDepth() * doorOrWindow.getWallDistance();
2680               } else {
2681                 wallDistance += piece.getDepth() * (doorOrWindow.getWallDistance() + doorOrWindow.getWallThickness())
2682                     - referenceWall.getThickness();
2683               }
2684             }
2685           } else {
2686             // Place the window in the middle of the round wall
2687             wallDistance = -referenceWall.getThickness() / 2;
2688             if (piece instanceof HomeDoorOrWindow) {
2689               // Place the window part in the middle of the round wall
2690               HomeDoorOrWindow doorOrWindow = (HomeDoorOrWindow)piece;
2691               wallDistance += piece.getDepth() * (doorOrWindow.getWallDistance() + doorOrWindow.getWallThickness() / 2);
2692             }
2693           }
2694           if (distanceToRightSide < distanceToLeftSide) {
2695             xPiece += sinWallAngle * ( (distanceToLeftSide + wallDistance) - halfDepth);
2696             yPiece += cosWallAngle * (-(distanceToLeftSide + wallDistance) + halfDepth);
2697           } else {
2698             xPiece += sinWallAngle * (-(distanceToRightSide + wallDistance) + halfDepth);
2699             yPiece += cosWallAngle * ( (distanceToRightSide + wallDistance) - halfDepth);
2700           }
2701           if (magnetizedAtRight) {
2702             xPiece += cosWallAngle * (halfWidth - distanceToPieceSide);
2703             yPiece += sinWallAngle * (halfWidth - distanceToPieceSide);
2704           } else {
2705             // Ensure adjusted window is at the right of the cursor
2706             xPiece += -cosWallAngle * (halfWidth - distanceToPieceSide);
2707             yPiece += -sinWallAngle * (halfWidth - distanceToPieceSide);
2708           }
2709         } else {
2710           if (distanceToRightSide < distanceToLeftSide) {
2711             int pointIndicator = Line2D.relativeCCW(
2712                 wallPoints [2][0], wallPoints [2][1], wallPoints [3][0], wallPoints [3][1], x, y);
2713             xPiece +=  pointIndicator * sinWallAngle * distanceToRightSide - sinWallAngle * halfDepth;
2714             yPiece += -pointIndicator * cosWallAngle * distanceToRightSide + cosWallAngle * halfDepth;
2715           } else {
2716             int pointIndicator = Line2D.relativeCCW(
2717                 wallPoints [0][0], wallPoints [0][1], wallPoints [1][0], wallPoints [1][1], x, y);
2718             xPiece += -pointIndicator * sinWallAngle * distanceToLeftSide + sinWallAngle * halfDepth;
2719             yPiece +=  pointIndicator * cosWallAngle * distanceToLeftSide - cosWallAngle * halfDepth;
2720           }
2721           if (magnetizedAtRight) {
2722             xPiece += cosWallAngle * (halfWidth - distanceToPieceSide);
2723             yPiece += sinWallAngle * (halfWidth - distanceToPieceSide);
2724           } else {
2725             // Ensure adjusted piece is at the right of the cursor
2726             xPiece += -cosWallAngle * (halfWidth - distanceToPieceSide);
2727             yPiece += -sinWallAngle * (halfWidth - distanceToPieceSide);
2728           }
2729         }
2730       } else {
2731         // Search the distance required to align piece on the left or right side of the reference wall
2732         Line2D centerLine = new Line2D.Float(referenceWall.getXStart(), referenceWall.getYStart(),
2733             referenceWall.getXEnd(), referenceWall.getYEnd());
2734         Shape pieceBoundingBox = getRotatedRectangle(0, 0, piece.getWidthInPlan(), piece.getDepthInPlan(), (float)(pieceAngle - wallAngle));
2735         double rotatedBoundingBoxDepth = pieceBoundingBox.getBounds2D().getHeight();
2736         float relativeCCWToPieceCenterSignum = Math.signum(centerLine.relativeCCW(piece.getX(), piece.getY()));
2737         float relativeCCWToPointSignum = Math.signum(centerLine.relativeCCW(x, y));
2738         double distance = relativeCCWToPieceCenterSignum
2739             * (-referenceWall.getThickness() / 2 + centerLine.ptLineDist(piece.getX(), piece.getY()) - rotatedBoundingBoxDepth / 2);
2740         if (includeBaseboards) {
2741           if (relativeCCWToPieceCenterSignum > 0
2742               && referenceWall.getLeftSideBaseboard() != null) {
2743             distance -= relativeCCWToPieceCenterSignum * referenceWall.getLeftSideBaseboard().getThickness();
2744           } else if (relativeCCWToPieceCenterSignum < 0
2745                      && referenceWall.getRightSideBaseboard() != null) {
2746             distance -= relativeCCWToPieceCenterSignum * referenceWall.getRightSideBaseboard().getThickness();
2747           }
2748         }
2749         if (relativeCCWToPointSignum != relativeCCWToPieceCenterSignum) {
2750           distance -= relativeCCWToPointSignum * (rotatedBoundingBoxDepth + referenceWall.getThickness());
2751           if (referenceWall.getLeftSideBaseboard() != null) {
2752             distance -= relativeCCWToPointSignum * referenceWall.getLeftSideBaseboard().getThickness();
2753           }
2754           if (referenceWall.getRightSideBaseboard() != null) {
2755             distance -= relativeCCWToPointSignum * referenceWall.getRightSideBaseboard().getThickness();
2756           }
2757         }
2758         xPiece = piece.getX() + (float)(-distance * sinWallAngle);
2759         yPiece = piece.getY() + (float)(distance * cosWallAngle);
2760       }
2761 
2762       if (!piece.isDoorOrWindow()
2763           && (referenceWall.getArcExtent() == null // Ignore reoriented piece when (x, y) is inside a round wall
2764               || !adjustOrientation
2765               || Line2D.relativeCCW(referenceWall.getXStart(), referenceWall.getYStart(),
2766                     referenceWall.getXEnd(), referenceWall.getYEnd(), x, y) > 0)) {
2767         // Search if piece intersects some other walls and avoid it intersects the closest one
2768         Area wallsAreaIntersection = new Area(wallsArea);
2769         Area adjustedPieceArea = new Area(getRotatedRectangle(xPiece - halfWidth,
2770                 yPiece - halfDepth, piece.getWidthInPlan(), piece.getDepthInPlan(), pieceAngle));
2771         wallsAreaIntersection.subtract(new Area(getPath(wallPoints)));
2772         wallsAreaIntersection.intersect(adjustedPieceArea);
2773         if (!wallsAreaIntersection.isEmpty()) {
2774           // Search the wall intersection path the closest to (x, y)
2775           GeneralPath closestWallIntersectionPath = getClosestPath(getAreaPaths(wallsAreaIntersection), x, y);
2776           if (closestWallIntersectionPath != null) {
2777             // In case the adjusted piece crosses a wall, search the area intersecting that wall
2778             // + other parts which crossed the wall (the farthest ones from (x,y))
2779             adjustedPieceArea.subtract(wallsArea);
2780             if (adjustedPieceArea.isEmpty()) {
2781               return null;
2782             } else {
2783               List<GeneralPath> adjustedPieceAreaPaths = getAreaPaths(adjustedPieceArea);
2784               // Ignore too complex cases when the piece intersect many walls and is not parallel to a wall
2785               double angleDifference = (wallAngle - pieceAngle + 2 * Math.PI) % Math.PI;
2786               if (angleDifference < 1E-5
2787                   || Math.PI - angleDifference < 1E-5
2788                   || adjustedPieceAreaPaths.size() < 2) {
2789                 GeneralPath adjustedPiecePathInArea = getClosestPath(adjustedPieceAreaPaths, x, y);
2790                 Area adjustingArea = new Area(closestWallIntersectionPath);
2791                 for (GeneralPath path : adjustedPieceAreaPaths) {
2792                   if (path != adjustedPiecePathInArea) {
2793                     adjustingArea.add(new Area(path));
2794                   }
2795                 }
2796                 AffineTransform rotation = AffineTransform.getRotateInstance(-wallAngle);
2797                 Rectangle2D adjustingAreaBounds = adjustingArea.createTransformedArea(rotation).getBounds2D();
2798                 Rectangle2D adjustedPiecePathInAreaBounds = adjustedPiecePathInArea.createTransformedShape(rotation).getBounds2D();
2799                 if (!adjustingAreaBounds.contains(adjustedPiecePathInAreaBounds)) {
2800                   double adjustLeftBorder = Math.signum(adjustedPiecePathInAreaBounds.getCenterX() - adjustingAreaBounds.getCenterX());
2801                   xPiece += adjustingAreaBounds.getWidth() * cosWallAngle * adjustLeftBorder;
2802                   yPiece += adjustingAreaBounds.getWidth() * sinWallAngle * adjustLeftBorder;
2803                 }
2804               }
2805             }
2806           }
2807         }
2808       }
2809 
2810       piece.setAngle(pieceAngle);
2811       piece.setX(xPiece);
2812       piece.setY(yPiece);
2813       if (piece instanceof HomeDoorOrWindow) {
2814         ((HomeDoorOrWindow)piece).setBoundToWall(referenceWallArcExtent == null
2815             || referenceWallArcExtent.floatValue() == 0);
2816       }
2817       return referenceWall;
2818     }
2819 
2820     return null;
2821   }
2822 
2823   /**
2824    * Returns <code>true</code> is the given <code>level</code> is viewable.
2825    */
2826   private boolean isLevelNullOrViewable(Level level) {
2827     return level == null || level.isViewable();
2828   }
2829 
2830   /**
2831    * Returns <code>wall</code> or a small wall part at the angle formed by the line joining wall center to
2832    * (<code>x</code>, <code>y</code>) point if the given <code>wall</code> is round.
2833    */
2834   private Wall getReferenceWall(Wall wall, float x, float y) {
2835     Float arcExtent = wall.getArcExtent();
2836     if (arcExtent == null || arcExtent.floatValue() == 0) {
2837       return wall;
2838     } else {
2839       double angle = Math.atan2(wall.getYArcCircleCenter() - y, x - wall.getXArcCircleCenter());
2840       double radius = Point2D.distance(wall.getXArcCircleCenter(), wall.getYArcCircleCenter(), wall.getXStart(), wall.getYStart());
2841       float epsilonAngle = 0.001f;
2842       Wall wallPart = new Wall((float)(wall.getXArcCircleCenter() + Math.cos(angle + epsilonAngle) * radius),
2843           (float)(wall.getYArcCircleCenter() - Math.sin(angle + epsilonAngle) * radius),
2844           (float)(wall.getXArcCircleCenter() + Math.cos(angle - epsilonAngle) * radius),
2845           (float)(wall.getYArcCircleCenter() - Math.sin(angle - epsilonAngle) * radius), wall.getThickness(), 0);
2846       wallPart.setLeftSideBaseboard(wall.getLeftSideBaseboard());
2847       wallPart.setRightSideBaseboard(wall.getRightSideBaseboard());
2848       return wallPart;
2849     }
2850   }
2851 
2852   /**
2853    * Returns the closest path among <code>paths</code> ones to the given point.
2854    */
2855   private GeneralPath getClosestPath(List<GeneralPath> paths, float x, float y) {
2856     GeneralPath closestPath = null;
2857     double closestPathDistance = Double.MAX_VALUE;
2858     for (GeneralPath path : paths) {
2859       float [][] pathPoints = getPathPoints(path, true);
2860       for (int i = 0; i < pathPoints.length; i++) {
2861         double distanceToPath = Line2D.ptSegDistSq(pathPoints [i][0], pathPoints [i][1],
2862             pathPoints [(i + 1) % pathPoints.length][0], pathPoints [(i + 1) % pathPoints.length][1], x, y);
2863         if (distanceToPath < closestPathDistance) {
2864           closestPathDistance = distanceToPath;
2865           closestPath = path;
2866         }
2867       }
2868     }
2869     return closestPath;
2870   }
2871 
2872   /**
2873    * Returns the dimension lines that indicates how is placed a given <code>piece</code>
2874    * along a <code>wall</code>.
2875    */
2876   private List<DimensionLine> getDimensionLinesAlongWall(HomePieceOfFurniture piece, Wall wall) {
2877     // Search the points on the wall side closest to piece
2878     float [][] piecePoints = piece.getPoints();
2879     float angle = piece.getAngle();
2880     float [][] wallPoints = wall.getPoints();
2881     float [] pieceLeftPoint;
2882     float [] pieceRightPoint;
2883     float [] piecePoint = piece.isDoorOrWindow()
2884         ? piecePoints [3] // Front side point
2885         : piecePoints [0]; // Back side point
2886     if (Line2D.ptLineDistSq(wallPoints [0][0], wallPoints [0][1],
2887             wallPoints [1][0], wallPoints [1][1],
2888             piecePoint [0], piecePoint [1])
2889         <= Line2D.ptLineDistSq(wallPoints [2][0], wallPoints [2][1],
2890             wallPoints [3][0], wallPoints [3][1],
2891             piecePoint [0], piecePoint [1])) {
2892       pieceLeftPoint = computeIntersection(wallPoints [0], wallPoints [1], piecePoints [0], piecePoints [3]);
2893       pieceRightPoint = computeIntersection(wallPoints [0], wallPoints [1], piecePoints [1], piecePoints [2]);
2894     } else {
2895       pieceLeftPoint = computeIntersection(wallPoints [2], wallPoints [3], piecePoints [0], piecePoints [3]);
2896       pieceRightPoint = computeIntersection(wallPoints [2], wallPoints [3], piecePoints [1], piecePoints [2]);
2897     }
2898 
2899     List<DimensionLine> dimensionLines = new ArrayList<DimensionLine>();
2900     float [] wallEndPointJoinedToPieceLeftPoint = null;
2901     float [] wallEndPointJoinedToPieceRightPoint = null;
2902     // Search among room paths which segment includes pieceLeftPoint and pieceRightPoint
2903     List<GeneralPath> roomPaths = getRoomPathsFromWalls();
2904     for (int i = 0;
2905          i < roomPaths.size()
2906          && wallEndPointJoinedToPieceLeftPoint == null
2907          && wallEndPointJoinedToPieceRightPoint == null; i++) {
2908       float [][] roomPoints = getPathPoints(roomPaths.get(i), true);
2909       for (int j = 0; j < roomPoints.length; j++) {
2910         float [] startPoint = roomPoints [j];
2911         float [] endPoint = roomPoints [(j + 1) % roomPoints.length];
2912         float deltaX = endPoint [0] - startPoint [0];
2913         float deltaY = endPoint [1] - startPoint [1];
2914         double segmentAngle = Math.abs(deltaX) < 1E-5
2915             ? Math.PI / 2
2916             : (Math.abs(deltaY) < 1E-5
2917                   ? 0
2918                   : Math.atan2(deltaY, deltaX));
2919         // If segment and piece are parallel
2920         double angleDifference = (segmentAngle - angle + 2 * Math.PI) % Math.PI;
2921         if (angleDifference < 1E-5 || Math.PI - angleDifference < 1E-5) {
2922           boolean segmentContainsLeftPoint = Line2D.ptSegDistSq(startPoint [0], startPoint [1],
2923               endPoint [0], endPoint [1], pieceLeftPoint [0], pieceLeftPoint [1]) < 0.0001;
2924           boolean segmentContainsRightPoint = Line2D.ptSegDistSq(startPoint [0], startPoint [1],
2925               endPoint [0], endPoint [1], pieceRightPoint [0], pieceRightPoint [1]) < 0.0001;
2926           if (segmentContainsLeftPoint || segmentContainsRightPoint) {
2927             if (segmentContainsLeftPoint) {
2928               // Compute distances to segment start point
2929               double startPointToLeftPointDistance = Point2D.distanceSq(startPoint [0], startPoint [1],
2930                   pieceLeftPoint [0], pieceLeftPoint [1]);
2931               double startPointToRightPointDistance = Point2D.distanceSq(startPoint [0], startPoint [1],
2932                   pieceRightPoint [0], pieceRightPoint [1]);
2933               if (startPointToLeftPointDistance < startPointToRightPointDistance
2934                   || !segmentContainsRightPoint) {
2935                 wallEndPointJoinedToPieceLeftPoint = startPoint.clone();
2936               } else {
2937                 wallEndPointJoinedToPieceLeftPoint = endPoint.clone();
2938               }
2939             }
2940             if (segmentContainsRightPoint) {
2941               // Compute distances to segment start point
2942               double endPointToLeftPointDistance = Point2D.distanceSq(endPoint [0], endPoint [1],
2943                   pieceLeftPoint [0], pieceLeftPoint [1]);
2944               double endPointToRightPointDistance = Point2D.distanceSq(endPoint [0], endPoint [1],
2945                   pieceRightPoint [0], pieceRightPoint [1]);
2946               if (endPointToLeftPointDistance < endPointToRightPointDistance
2947                   && segmentContainsLeftPoint) {
2948                 wallEndPointJoinedToPieceRightPoint = startPoint.clone();
2949               } else {
2950                 wallEndPointJoinedToPieceRightPoint = endPoint.clone();
2951               }
2952             }
2953             break;
2954           }
2955         }
2956       }
2957     }
2958 
2959     boolean pieceFrontSideAlongWallSide = !piece.isDoorOrWindow()
2960         && Line2D.ptLineDistSq(wall.getXStart(), wall.getYStart(), wall.getXEnd(), wall.getYEnd(), piecePoint [0], piecePoint [1])
2961             > Line2D.ptLineDistSq(wall.getXStart(), wall.getYStart(), wall.getXEnd(), wall.getYEnd(), piecePoints [3][0], piecePoints [3][1]);
2962     if (wallEndPointJoinedToPieceLeftPoint != null) {
2963       float offset;
2964       if (pieceFrontSideAlongWallSide) {
2965         offset = -(float)Point2D.distance(pieceLeftPoint [0], pieceLeftPoint [1],
2966             piecePoints [0][0], piecePoints [0][1]) - 10 / getView().getScale();
2967       } else {
2968         offset = (float)Point2D.distance(pieceLeftPoint [0], pieceLeftPoint [1],
2969             piecePoints [3][0], piecePoints [3][1]) + 10 / getView().getScale();
2970       }
2971       dimensionLines.add(getDimensionLineBetweenPoints(wallEndPointJoinedToPieceLeftPoint, pieceLeftPoint, offset, 0, false));
2972     }
2973     if (wallEndPointJoinedToPieceRightPoint != null) {
2974       float offset;
2975       if (pieceFrontSideAlongWallSide) {
2976         offset = -(float)Point2D.distance(pieceRightPoint [0], pieceRightPoint [1],
2977             piecePoints [1][0], piecePoints [1][1]) - 10 / getView().getScale();
2978       } else {
2979         offset = (float)Point2D.distance(pieceRightPoint [0], pieceRightPoint [1],
2980             piecePoints [2][0], piecePoints [2][1]) + 10 / getView().getScale();
2981       }
2982       dimensionLines.add(getDimensionLineBetweenPoints(pieceRightPoint,  wallEndPointJoinedToPieceRightPoint, offset, 0, false));
2983     }
2984     for (int i = dimensionLines.size() - 1; i >= 0; i--) {
2985       if (dimensionLines.get(i).getLength() < 0.01f) {
2986         dimensionLines.remove(i);
2987       }
2988     }
2989     return dimensionLines;
2990   }
2991 
2992   /**
2993    * Returns the intersection point between the lines defined by the points
2994    * (<code>point1</code>, <code>point2</code>) and (<code>point3</code>, <code>pont4</code>).
2995    */
2996   private static float [] computeIntersection(float [] point1, float [] point2, float [] point3, float [] point4) {
2997     return computeIntersection(point1 [0], point1 [1], point2 [0], point2 [1],
2998         point3 [0], point3 [1], point4 [0], point4 [1]);
2999   }
3000 
3001   /**
3002    * Returns the intersection point between the line joining the first two points and
3003    * the line joining the two last points.
3004    */
3005   static float [] computeIntersection(float xPoint1, float yPoint1, float xPoint2, float yPoint2,
3006                                       float xPoint3, float yPoint3, float xPoint4, float yPoint4) {
3007     float x = xPoint2;
3008     float y = yPoint2;
3009     float alpha1 = (yPoint2 - yPoint1) / (xPoint2 - xPoint1);
3010     float alpha2 = (yPoint4 - yPoint3) / (xPoint4 - xPoint3);
3011     // If the two lines are not parallel
3012     if (alpha1 != alpha2) {
3013       // If first line is vertical
3014       if (Math.abs(alpha1) > 4000)  {
3015         if (Math.abs(alpha2) < 4000) {
3016           x = xPoint1;
3017           float beta2  = yPoint4 - alpha2 * xPoint4;
3018           y = alpha2 * x + beta2;
3019         }
3020       // If second line is vertical
3021       } else if (Math.abs(alpha2) > 4000) {
3022         if (Math.abs(alpha1) < 4000) {
3023           x = xPoint3;
3024           float beta1  = yPoint2 - alpha1 * xPoint2;
3025           y = alpha1 * x + beta1;
3026         }
3027       } else {
3028         boolean sameSignum = Math.signum(alpha1) == Math.signum(alpha2);
3029         if (Math.abs(alpha1 - alpha2) > 1E-5
3030             && (!sameSignum || (Math.abs(alpha1) > Math.abs(alpha2)   ? alpha1 / alpha2   : alpha2 / alpha1) > 1.004)) {
3031           float beta1  = yPoint2 - alpha1 * xPoint2;
3032           float beta2  = yPoint4 - alpha2 * xPoint4;
3033           x = (beta2 - beta1) / (alpha1 - alpha2);
3034           y = alpha1 * x + beta1;
3035         }
3036       }
3037     }
3038     return new float [] {x, y};
3039   }
3040 
3041   /**
3042    * Attempts to elevate <code>piece</code> depending on the highest piece that includes
3043    * its bounding box and returns that piece.
3044    * @see #adjustMagnetizedPieceOfFurniture(HomePieceOfFurniture, float, float)
3045    */
3046   private HomePieceOfFurniture adjustPieceOfFurnitureElevation(HomePieceOfFurniture piece) {
3047     if (!piece.isDoorOrWindow()
3048         && piece.getElevation() == 0) {
3049       // Search if another piece at floor level contains the given piece to elevate it at its height
3050       HomePieceOfFurniture highestSurroundingPiece = getHighestSurroundingPieceOfFurniture(piece);
3051       if (highestSurroundingPiece != null) {
3052         float elevation = highestSurroundingPiece.getElevation();
3053         if (highestSurroundingPiece.isHorizontallyRotated()) {
3054           elevation += highestSurroundingPiece.getHeightInPlan();
3055         } else {
3056           elevation += highestSurroundingPiece.getHeight() * highestSurroundingPiece.getDropOnTopElevation();
3057         }
3058         if (highestSurroundingPiece.getLevel() != null) {
3059           elevation += highestSurroundingPiece.getLevel().getElevation()
3060               - (piece.getLevel() != null
3061                     ? piece.getLevel().getElevation()
3062                     : this.home.getSelectedLevel().getElevation());
3063         }
3064         piece.setElevation(Math.max(0, elevation));
3065         return highestSurroundingPiece;
3066       }
3067     }
3068     return null;
3069   }
3070 
3071   /**
3072    * Attempts to align <code>piece</code> on the borders of home furniture at the the same elevation
3073    * that intersect with it and returns that piece.
3074    * @see #adjustMagnetizedPieceOfFurniture(HomePieceOfFurniture, float, float)
3075    */
3076   private HomePieceOfFurniture adjustPieceOfFurnitureSideBySideAt(HomePieceOfFurniture piece,
3077                                                                   boolean forceOrientation,
3078                                                                   Wall magnetWall) {
3079     float [][] piecePoints = piece.getPoints();
3080     Area pieceArea = new Area(getPath(piecePoints));
3081     boolean doorOrWindowBoundToWall = piece instanceof HomeDoorOrWindow
3082         && ((HomeDoorOrWindow)piece).isBoundToWall();
3083 
3084     // Search if the border of another piece at floor level intersects with the given piece
3085     float pieceElevation = piece.getGroundElevation();
3086     float margin = 2 * PIXEL_MARGIN / getScale();
3087     HomePieceOfFurniture referencePiece = null;
3088     Area intersectionWithReferencePieceArea = null;
3089     float intersectionWithReferencePieceSurface = 0;
3090     float [][] referencePiecePoints = null;
3091     for (HomePieceOfFurniture homePiece : this.home.getFurniture()) {
3092       float homePieceElevation = homePiece.getGroundElevation();
3093       if (homePiece != piece
3094           && isPieceOfFurnitureVisibleAtSelectedLevel(homePiece)
3095           && pieceElevation < homePieceElevation + homePiece.getHeightInPlan()
3096           && pieceElevation + piece.getHeightInPlan() > homePieceElevation
3097           && (!doorOrWindowBoundToWall // Ignore other furniture for doors and windows bound to a wall
3098               || homePiece.isDoorOrWindow())) {
3099         float [][] points = homePiece.getPoints();
3100         Area marginArea;
3101         if (doorOrWindowBoundToWall && homePiece.isDoorOrWindow()) {
3102           marginArea = new Area(getPath(new Wall(
3103               points [1][0], points [1][1], points [2][0], points [2][1], margin, 0).getPoints()));
3104           marginArea.add(new Area(getPath(new Wall(
3105               points [3][0], points [3][1], points [0][0], points [0][1], margin, 0).getPoints())));
3106         } else {
3107           // Build an area matching piece contour with a thickness equal to margin using walls instances
3108           marginArea = this.furnitureSidesCache.get(homePiece);
3109           if (marginArea == null) {
3110             Wall [] pieceSideWalls = new Wall [points.length];
3111             for (int i = 0; i < pieceSideWalls.length; i++) {
3112               pieceSideWalls [i] = new Wall(points [i][0], points [i][1],
3113                   points [(i + 1) % pieceSideWalls.length][0], points [(i + 1) % pieceSideWalls.length][1], margin, 0);
3114             }
3115             for (int i = 0; i < pieceSideWalls.length; i++) {
3116               pieceSideWalls [(i + 1) % pieceSideWalls.length].setWallAtStart(pieceSideWalls [i]);
3117               pieceSideWalls [i].setWallAtEnd(pieceSideWalls [(i + 1) % pieceSideWalls.length]);
3118             }
3119             float [][] pieceSidePoints = new float [pieceSideWalls.length * 2 + 2][];
3120             float [][] pieceSideWallPoints = null;
3121             for (int i = 0; i < pieceSideWalls.length; i++) {
3122               pieceSideWallPoints = pieceSideWalls [i].getPoints();
3123               pieceSidePoints [i] = pieceSideWallPoints [0];
3124               pieceSidePoints [pieceSidePoints.length - i - 1] = pieceSideWallPoints [3];
3125             }
3126             pieceSidePoints [pieceSidePoints.length / 2 - 1] = pieceSideWallPoints [1];
3127             pieceSidePoints [pieceSidePoints.length / 2] = pieceSideWallPoints [2];
3128             marginArea = new Area(getPath(pieceSidePoints));
3129             this.furnitureSidesCache.put(homePiece, marginArea);
3130           }
3131         }
3132         Area intersection = new Area(marginArea);
3133         intersection.intersect(pieceArea);
3134         if (!intersection.isEmpty()) {
3135           Area exclusiveOr = new Area(pieceArea);
3136           exclusiveOr.exclusiveOr(intersection);
3137           if (exclusiveOr.isSingular()) {
3138             Area insideArea = new Area(getPath(points));
3139             insideArea.subtract(marginArea);
3140             insideArea.intersect(pieceArea);
3141             if (insideArea.isEmpty()) {
3142               float surface = getArea(intersection);
3143               if (surface > intersectionWithReferencePieceSurface) {
3144                 intersectionWithReferencePieceSurface = surface;
3145                 referencePiece = homePiece;
3146                 referencePiecePoints = points;
3147                 intersectionWithReferencePieceArea = intersection;
3148               }
3149             }
3150           }
3151         }
3152       }
3153     }
3154 
3155     if (referencePiece != null) {
3156       boolean alignedOnReferencePieceFrontOrBackSide;
3157       if (doorOrWindowBoundToWall && referencePiece.isDoorOrWindow()) {
3158         alignedOnReferencePieceFrontOrBackSide = false;
3159       } else {
3160         GeneralPath referencePieceLargerBoundingBox = getRotatedRectangle(referencePiece.getX() - referencePiece.getWidthInPlan(),
3161             referencePiece.getY() - referencePiece.getDepthInPlan(), referencePiece.getWidthInPlan() * 2, referencePiece.getDepthInPlan() * 2,
3162             referencePiece.getAngle());
3163         float [][] pathPoints = getPathPoints(referencePieceLargerBoundingBox, false);
3164         alignedOnReferencePieceFrontOrBackSide = isAreaLargerOnFrontOrBackSide(intersectionWithReferencePieceArea, pathPoints);
3165       }
3166       if (forceOrientation) {
3167         piece.setAngle(referencePiece.getAngle());
3168       }
3169       Shape pieceBoundingBox = getRotatedRectangle(0, 0, piece.getWidthInPlan(), piece.getDepthInPlan(), piece.getAngle() - referencePiece.getAngle());
3170       float deltaX = 0;
3171       float deltaY = 0;
3172       if (!alignedOnReferencePieceFrontOrBackSide) {
3173         // Search the distance required to align piece on the left or right side of the reference piece
3174         Line2D centerLine = new Line2D.Float(referencePiece.getX(), referencePiece.getY(),
3175             (referencePiecePoints [0][0] + referencePiecePoints [1][0]) / 2, (referencePiecePoints [0][1] + referencePiecePoints [1][1]) / 2);
3176         double rotatedBoundingBoxWidth = pieceBoundingBox.getBounds2D().getWidth();
3177         double distance = centerLine.relativeCCW(piece.getX(), piece.getY())
3178             * (-referencePiece.getWidthInPlan() / 2 + centerLine.ptLineDist(piece.getX(), piece.getY()) - rotatedBoundingBoxWidth / 2);
3179         deltaX = (float)(distance * Math.cos(referencePiece.getAngle()));
3180         deltaY = (float)(distance * Math.sin(referencePiece.getAngle()));
3181       } else {
3182         // Search the distance required to align piece on the front or back side of the reference piece
3183         Line2D centerLine = new Line2D.Float(referencePiece.getX(), referencePiece.getY(),
3184             (referencePiecePoints [2][0] + referencePiecePoints [1][0]) / 2, (referencePiecePoints [2][1] + referencePiecePoints [1][1]) / 2);
3185         double rotatedBoundingBoxDepth = pieceBoundingBox.getBounds2D().getHeight();
3186         double distance = centerLine.relativeCCW(piece.getX(), piece.getY())
3187             * (-referencePiece.getDepthInPlan() / 2 + centerLine.ptLineDist(piece.getX(), piece.getY()) - rotatedBoundingBoxDepth / 2);
3188         deltaX = (float)(-distance * Math.sin(referencePiece.getAngle()));
3189         deltaY = (float)(distance * Math.cos(referencePiece.getAngle()));
3190         if (!isIntersectionEmpty(piece, magnetWall, deltaX, deltaY)) {
3191           deltaX = deltaY = 0;
3192         }
3193       }
3194 
3195       // Accept move only if reference piece and moved piece share some points
3196       if (!isIntersectionEmpty(piece, referencePiece, deltaX, deltaY)) {
3197         piece.move(deltaX, deltaY);
3198         return referencePiece;
3199       } else {
3200         if (forceOrientation) {
3201           // Update points array
3202           piecePoints = piece.getPoints();
3203         }
3204         boolean alignedOnPieceFrontOrBackSide = isAreaLargerOnFrontOrBackSide(intersectionWithReferencePieceArea, piecePoints);
3205         Shape referencePieceBoundingBox = getRotatedRectangle(0, 0, referencePiece.getWidthInPlan(), referencePiece.getDepthInPlan(),
3206             referencePiece.getAngle() - piece.getAngle());
3207         if (!alignedOnPieceFrontOrBackSide) {
3208           // Search the distance required to align piece on its left or right side
3209           Line2D centerLine = new Line2D.Float(piece.getX(), piece.getY(),
3210               (piecePoints [0][0] + piecePoints [1][0]) / 2, (piecePoints [0][1] + piecePoints [1][1]) / 2);
3211           double rotatedBoundingBoxWidth = referencePieceBoundingBox.getBounds2D().getWidth();
3212           double distance = centerLine.relativeCCW(referencePiece.getX(), referencePiece.getY())
3213               * (-piece.getWidthInPlan() / 2 + centerLine.ptLineDist(referencePiece.getX(), referencePiece.getY()) - rotatedBoundingBoxWidth / 2);
3214           deltaX = -(float)(distance * Math.cos(piece.getAngle()));
3215           deltaY = -(float)(distance * Math.sin(piece.getAngle()));
3216         } else {
3217           // Search the distance required to align piece on its front or back side
3218           Line2D centerLine = new Line2D.Float(piece.getX(), piece.getY(),
3219               (piecePoints [2][0] + piecePoints [1][0]) / 2, (piecePoints [2][1] + piecePoints [1][1]) / 2);
3220           double rotatedBoundingBoxDepth = referencePieceBoundingBox.getBounds2D().getHeight();
3221           double distance = centerLine.relativeCCW(referencePiece.getX(), referencePiece.getY())
3222               * (-piece.getDepthInPlan() / 2 + centerLine.ptLineDist(referencePiece.getX(), referencePiece.getY()) - rotatedBoundingBoxDepth / 2);
3223           deltaX = -(float)(-distance * Math.sin(piece.getAngle()));
3224           deltaY = -(float)(distance * Math.cos(piece.getAngle()));
3225           if (!isIntersectionEmpty(piece, magnetWall, deltaX, deltaY)) {
3226             deltaX = deltaY = 0;
3227           }
3228         }
3229 
3230         // Accept move only if reference piece and moved piece share some points
3231         if (!isIntersectionEmpty(piece, referencePiece, deltaX, deltaY)) {
3232           piece.move(deltaX, deltaY);
3233           return referencePiece;
3234         }
3235       }
3236       return referencePiece;
3237     }
3238     return null;
3239   }
3240 
3241   /**
3242    * Returns <code>true</code> if the intersection between the given <code>area</code> and
3243    * the front or back sides of the rectangle defined by <code>piecePoints</code> is larger
3244    * than with the left and right sides of this rectangle.
3245    */
3246   private boolean isAreaLargerOnFrontOrBackSide(Area area, float [][] piecePoints) {
3247     GeneralPath pieceFrontAndBackQuarters = new GeneralPath();
3248     pieceFrontAndBackQuarters.moveTo(piecePoints [0][0], piecePoints [0][1]);
3249     pieceFrontAndBackQuarters.lineTo(piecePoints [2][0], piecePoints [2][1]);
3250     pieceFrontAndBackQuarters.lineTo(piecePoints [3][0], piecePoints [3][1]);
3251     pieceFrontAndBackQuarters.lineTo(piecePoints [1][0], piecePoints [1][1]);
3252     pieceFrontAndBackQuarters.closePath();
3253     Area intersectionWithFrontOrBack = new Area(area);
3254     intersectionWithFrontOrBack.intersect(new Area(pieceFrontAndBackQuarters));
3255     if (intersectionWithFrontOrBack.isEmpty()) {
3256       return false;
3257     } else {
3258       GeneralPath pieceLeftAndRightQuarters = new GeneralPath();
3259       pieceLeftAndRightQuarters.moveTo(piecePoints [0][0], piecePoints [0][1]);
3260       pieceLeftAndRightQuarters.lineTo(piecePoints [2][0], piecePoints [2][1]);
3261       pieceLeftAndRightQuarters.lineTo(piecePoints [1][0], piecePoints [1][1]);
3262       pieceLeftAndRightQuarters.lineTo(piecePoints [3][0], piecePoints [3][1]);
3263       pieceLeftAndRightQuarters.closePath();
3264       Area intersectionWithLeftAndRight = new Area(area);
3265       intersectionWithLeftAndRight.intersect(new Area(pieceLeftAndRightQuarters));
3266       return getArea(intersectionWithFrontOrBack) > getArea(intersectionWithLeftAndRight);
3267     }
3268   }
3269 
3270   /**
3271    * Returns the area of the given shape.
3272    */
3273   private float getArea(Area area) {
3274     float [][] pathPoints = getPathPoints(getPath(area), false);
3275     if (pathPoints.length > 1) {
3276       return new Room(pathPoints).getArea();
3277     } else {
3278       return 0;
3279     }
3280   }
3281 
3282   /**
3283    * Returns <code>true</code> if the given pieces don't intersect once the first is moved from
3284    * (<code>deltaX</code>, <code>deltaY</code>) vector.
3285    */
3286   private boolean isIntersectionEmpty(HomePieceOfFurniture piece1, HomePieceOfFurniture piece2,
3287                                       float deltaX, float deltaY) {
3288     Area intersection = new Area(getRotatedRectangle(piece1.getX() - piece1.getWidthInPlan() / 2 + deltaX,
3289         piece1.getY() - piece1.getDepthInPlan() / 2 + deltaY, piece1.getWidthInPlan(), piece1.getDepth(), piece1.getAngle()));
3290     float epsilon = 0.01f;
3291     intersection.intersect(new Area(getRotatedRectangle(piece2.getX() - piece2.getWidthInPlan() / 2 - epsilon,
3292         piece2.getY() - piece2.getDepthInPlan() / 2 - epsilon,
3293         piece2.getWidthInPlan() + 2 * epsilon, piece2.getDepthInPlan() + 2 * epsilon, piece2.getAngle())));
3294     return intersection.isEmpty();
3295   }
3296 
3297   /**
3298    * Returns <code>true</code> if the given area and wall don't intersect once the area is moved from
3299    * (<code>deltaX</code>, <code>deltaY</code>) vector.
3300    */
3301   private boolean isIntersectionEmpty(HomePieceOfFurniture piece, Wall wall,
3302                                       float deltaX, float deltaY) {
3303     if (wall != null) {
3304       Area wallAreaIntersection = new Area(getPath(wall.getPoints()));
3305       wallAreaIntersection.intersect(new Area(getRotatedRectangle(piece.getX() - piece.getWidthInPlan() / 2 + deltaX,
3306           piece.getY() - piece.getDepthInPlan() / 2 + deltaY, piece.getWidthInPlan(), piece.getDepthInPlan(), piece.getAngle())));
3307       return getArea(wallAreaIntersection) < 1E-4f;
3308     }
3309     return true;
3310   }
3311 
3312   /**
3313    * Returns the shape of the given rectangle rotated of a given <code>angle</code>.
3314    */
3315   private GeneralPath getRotatedRectangle(float x, float y, float width, float height, float angle) {
3316     Rectangle2D referencePieceLargerBoundingBox = new Rectangle2D.Float(x, y, width, height);
3317     AffineTransform rotation = AffineTransform.getRotateInstance(angle, x + width / 2, y + height / 2);
3318     GeneralPath rotatedBoundingBox = new GeneralPath();
3319     rotatedBoundingBox.append(referencePieceLargerBoundingBox.getPathIterator(rotation), false);
3320     return rotatedBoundingBox;
3321   }
3322 
3323   /**
3324    * Returns the dimension line that measures the side of a piece, the length of a room side
3325    * or the length of a wall side at (<code>x</code>, <code>y</code>) point,
3326    * or <code>null</code> if it doesn't exist.
3327    */
3328   private DimensionLine getMeasuringDimensionLineAt(float x, float y,
3329                                                     boolean magnetismEnabled) {
3330     float margin = getSelectionMargin();
3331     for (HomePieceOfFurniture piece : this.home.getFurniture()) {
3332       if (isPieceOfFurnitureVisibleAtSelectedLevel(piece)) {
3333         DimensionLine dimensionLine = getDimensionLineBetweenPointsAt(piece.getPoints(), x, y, margin, magnetismEnabled);
3334         if (dimensionLine != null) {
3335           return dimensionLine;
3336         }
3337       }
3338     }
3339     for (GeneralPath roomPath : getRoomPathsFromWalls()) {
3340       if (roomPath.intersects(x - margin, y - margin, 2 * margin, 2 * margin)) {
3341         DimensionLine dimensionLine = getDimensionLineBetweenPointsAt(
3342             getPathPoints(roomPath, true), x, y, margin, magnetismEnabled);
3343         if (dimensionLine != null) {
3344           return dimensionLine;
3345         }
3346       }
3347     }
3348     for (Room room : this.home.getRooms()) {
3349       if (isLevelNullOrViewable(room.getLevel())
3350           && room.isAtLevel(this.home.getSelectedLevel())) {
3351         DimensionLine dimensionLine = getDimensionLineBetweenPointsAt(room.getPoints(), x, y, margin, magnetismEnabled);
3352         if (dimensionLine != null) {
3353           return dimensionLine;
3354         }
3355       }
3356     }
3357     return null;
3358   }
3359 
3360   /**
3361    * Returns the dimension line that measures the side of the given polygon at (<code>x</code>, <code>y</code>) point,
3362    * or <code>null</code> if it doesn't exist.
3363    */
3364   private DimensionLine getDimensionLineBetweenPointsAt(float [][] points, float x, float y,
3365                                                         float margin, boolean magnetismEnabled) {
3366     for (int i = 0; i < points.length; i++) {
3367       int nextPointIndex = (i + 1) % points.length;
3368       // Ignore sides with a length smaller than 0.1 cm
3369       double distanceBetweenPointsSq = Point2D.distanceSq(points [i][0], points [i][1],
3370               points [nextPointIndex][0], points [nextPointIndex][1]);
3371       if (distanceBetweenPointsSq > 0.01
3372           && Line2D.ptSegDistSq(points [i][0], points [i][1],
3373               points [nextPointIndex][0], points [nextPointIndex][1],
3374               x, y) <= margin * margin) {
3375         return getDimensionLineBetweenPoints(points [i], points [nextPointIndex], 0, distanceBetweenPointsSq, magnetismEnabled);
3376       }
3377     }
3378     return null;
3379   }
3380 
3381   /**
3382    * Returns the dimension line between the given points.
3383    */
3384   private DimensionLine getDimensionLineBetweenPoints(float [] point1, float [] point2, float offset,
3385                                                       boolean magnetismEnabled) {
3386     return getDimensionLineBetweenPoints(point1, point2, offset,
3387         Point2D.distanceSq(point1 [0], point1 [1], point2 [0], point2 [1]), magnetismEnabled);
3388   }
3389 
3390   /**
3391    * Returns the dimension line between the given points.
3392    */
3393   private DimensionLine getDimensionLineBetweenPoints(float [] point1, float [] point2, float offset,
3394                                                       double distanceBetweenPointsSq,
3395                                                       boolean magnetismEnabled) {
3396     double angle = Math.atan2(point1 [1] - point2 [1], point2 [0] - point1 [0]);
3397     boolean reverse = angle <= -Math.PI / 2 || angle > Math.PI / 2;
3398     float xStart;
3399     float yStart;
3400     float xEnd;
3401     float yEnd;
3402     if (reverse) {
3403       // Avoid reversed text on the dimension line
3404       xStart = point2 [0];
3405       yStart = point2 [1];
3406       xEnd = point1 [0];
3407       yEnd = point1 [1];
3408       offset = -offset;
3409     } else {
3410       xStart = point1 [0];
3411       yStart = point1 [1];
3412       xEnd = point2 [0];
3413       yEnd = point2 [1];
3414     }
3415 
3416     if (magnetismEnabled) {
3417       float magnetizedLength = this.preferences.getLengthUnit().getMagnetizedLength(
3418           (float)Math.sqrt(distanceBetweenPointsSq), getView().getPixelLength());
3419       if (reverse) {
3420         xEnd = point2 [0] - (float)(magnetizedLength * Math.cos(angle));
3421         yEnd = point2 [1] + (float)(magnetizedLength * Math.sin(angle));
3422       } else {
3423         xEnd = point1 [0] + (float)(magnetizedLength * Math.cos(angle));
3424         yEnd = point1 [1] - (float)(magnetizedLength * Math.sin(angle));
3425       }
3426     }
3427     return new DimensionLine(xStart, yStart, xEnd, yEnd, offset);
3428   }
3429 
3430   /**
3431    * Controls the creation of a new level.
3432    */
3433   public void addLevel() {
3434     addLevel(false);
3435   }
3436 
3437   /**
3438    * Controls the creation of a new level at same elevation.
3439    */
3440   public void addLevelAtSameElevation() {
3441     addLevel(true);
3442   }
3443 
3444   /**
3445    * Controls the creation of a level.
3446    */
3447   private void addLevel(boolean sameElevation) {
3448     final boolean allLevelsSelection = this.home.isAllLevelsSelection();
3449     List<Selectable> oldSelectedItems = this.home.getSelectedItems();
3450     final Selectable [] oldSelection =
3451         oldSelectedItems.toArray(new Selectable [oldSelectedItems.size()]);
3452     final Level oldSelectedLevel = this.home.getSelectedLevel();
3453     final BackgroundImage homeBackgroundImage = this.home.getBackgroundImage();
3454     List<Level> levels = this.home.getLevels();
3455     float newWallHeight = this.preferences.getNewWallHeight();
3456     float newFloorThickness = this.preferences.getNewFloorThickness();
3457     final Level level0;
3458     if (levels.isEmpty()) {
3459       // Create level 0
3460       String level0Name = this.preferences.getLocalizedString(PlanController.class, "levelName", 0);
3461       level0 = createLevel(level0Name, 0, newFloorThickness, newWallHeight);
3462       moveHomeItemsToLevel(level0);
3463       level0.setBackgroundImage(homeBackgroundImage);
3464       this.home.setBackgroundImage(null);
3465       levels = this.home.getLevels();
3466     } else {
3467       level0 = null;
3468     }
3469     String newLevelName = this.preferences.getLocalizedString(PlanController.class, "levelName", levels.size());
3470     final Level newLevel;
3471     if (sameElevation) {
3472       Level referencedLevel = level0 != null
3473           ? level0
3474           : this.home.getSelectedLevel();
3475       newLevel = createLevel(newLevelName, referencedLevel.getElevation(),
3476           referencedLevel.getFloorThickness(), referencedLevel.getHeight());
3477     } else {
3478       float newLevelElevation = levels.get(levels.size() - 1).getElevation()
3479           + newWallHeight + newFloorThickness;
3480       newLevel = createLevel(newLevelName, newLevelElevation, newFloorThickness, newWallHeight);
3481     }
3482     setSelectedLevel(newLevel);
3483     this.undoSupport.postEdit(new LevelAdditionUndoableEdit(this, this.home, this.preferences,
3484         oldSelection, allLevelsSelection, oldSelectedLevel, level0, homeBackgroundImage, newLevel));
3485   }
3486 
3487   /**
3488    * Undoable edit for level addition.
3489    */
3490   private static class LevelAdditionUndoableEdit extends LocalizedUndoableEdit {
3491     private final PlanController  controller;
3492     private final Home            home;
3493     private final Selectable []   oldSelection;
3494     private final boolean         allLevelsSelection;
3495     private final Level           oldSelectedLevel;
3496     private final Level           level0;
3497     private final BackgroundImage homeBackgroundImage;
3498     private final Level           newLevel;
3499 
3500     public LevelAdditionUndoableEdit(PlanController controller, Home home, UserPreferences preferences,
3501                                      Selectable [] oldSelection, boolean allLevelsSelection, Level oldSelectedLevel,
3502                                      Level level0, BackgroundImage homeBackgroundImage, Level newLevel) {
3503       super(preferences, PlanController.class, "undoAddLevel");
3504       this.controller = controller;
3505       this.home = home;
3506       this.oldSelection = oldSelection;
3507       this.allLevelsSelection = allLevelsSelection;
3508       this.oldSelectedLevel = oldSelectedLevel;
3509       this.level0 = level0;
3510       this.homeBackgroundImage = homeBackgroundImage;
3511       this.newLevel = newLevel;
3512     }
3513 
3514     @Override
3515     public void undo() throws CannotUndoException {
3516       super.undo();
3517       controller.setSelectedLevel(this.oldSelectedLevel);
3518       this.home.deleteLevel(this.newLevel);
3519       if (this.level0 != null) {
3520         this.home.setBackgroundImage(this.homeBackgroundImage);
3521         controller.moveHomeItemsToLevel(this.oldSelectedLevel);
3522         this.home.deleteLevel(this.level0);
3523       }
3524       controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.allLevelsSelection);
3525     }
3526 
3527     @Override
3528     public void redo() throws CannotRedoException {
3529       super.redo();
3530       if (this.level0 != null) {
3531         this.home.addLevel(this.level0);
3532         controller.moveHomeItemsToLevel(this.level0);
3533         this.level0.setBackgroundImage(this.homeBackgroundImage);
3534         this.home.setBackgroundImage(null);
3535       }
3536       this.home.addLevel(this.newLevel);
3537       controller.setSelectedLevel(this.newLevel);
3538     }
3539   }
3540 
3541   /**
3542    * Returns a new level added to home.
3543    */
3544   protected Level createLevel(String name, float elevation, float floorThickness, float height) {
3545     Level newLevel = new Level(name, elevation, floorThickness, height);
3546     this.home.addLevel(newLevel);
3547     return newLevel;
3548   }
3549 
3550   /**
3551    * Moves to the given <code>level</code> all existing furniture, walls, rooms, dimension lines
3552    * and labels.
3553    */
3554   private void moveHomeItemsToLevel(Level level) {
3555     for (HomePieceOfFurniture piece : this.home.getFurniture()) {
3556       piece.setLevel(level);
3557     }
3558     for (Wall wall : this.home.getWalls()) {
3559       wall.setLevel(level);
3560     }
3561     for (Room room : this.home.getRooms()) {
3562       room.setLevel(level);
3563     }
3564     for (Polyline polyline : this.home.getPolylines()) {
3565       polyline.setLevel(level);
3566     }
3567     for (DimensionLine dimensionLine : this.home.getDimensionLines()) {
3568       dimensionLine.setLevel(level);
3569     }
3570     for (Label label : this.home.getLabels()) {
3571       label.setLevel(level);
3572     }
3573   }
3574 
3575   /**
3576    * Toggles the viewability of the selected level.
3577    * @since 5.0
3578    */
3579   public void toggleSelectedLevelViewability() {
3580     final Level selectedLevel = this.home.getSelectedLevel();
3581     selectedLevel.setViewable(!selectedLevel.isViewable());
3582     this.undoSupport.postEdit(new LevelViewabilityModificationUndoableEdit(this, this.preferences, selectedLevel));
3583   }
3584 
3585   /**
3586    * Undoable edit for level viewability modification.
3587    */
3588   private static class LevelViewabilityModificationUndoableEdit extends LocalizedUndoableEdit {
3589     private final PlanController controller;
3590     private final Level          selectedLevel;
3591 
3592     public LevelViewabilityModificationUndoableEdit(PlanController controller, UserPreferences preferences,
3593                                                     Level selectedLevel) {
3594       super(preferences, PlanController.class, "undoModifyLevelViewabilityName");
3595       this.controller = controller;
3596       this.selectedLevel = selectedLevel;
3597     }
3598 
3599     @Override
3600     public void undo() throws CannotUndoException {
3601       super.undo();
3602       this.controller.setSelectedLevel(this.selectedLevel);
3603       this.selectedLevel.setViewable(!this.selectedLevel.isViewable());
3604     }
3605 
3606     @Override
3607     public void redo() throws CannotRedoException {
3608       super.redo();
3609       this.controller.setSelectedLevel(this.selectedLevel);
3610       this.selectedLevel.setViewable(!this.selectedLevel.isViewable());
3611     }
3612   }
3613 
3614   /**
3615    * Makes the selected level the only viewable one.
3616    * @since 6.0
3617    */
3618   public void setSelectedLevelOnlyViewable() {
3619     final Level [] viewableLevels = getLevels(true);
3620     final Level selectedLevel = this.home.getSelectedLevel();
3621     final boolean selectedLevelViewable = selectedLevel.isViewable();
3622     if (viewableLevels.length != 1
3623         || !selectedLevelViewable) {
3624       setLevelsViewability(viewableLevels, false);
3625       selectedLevel.setViewable(true);
3626       this.undoSupport.postEdit(new LevelsViewabilityModificationUndoableEdit(this, this.preferences,
3627           selectedLevel, selectedLevelViewable, viewableLevels));
3628     }
3629   }
3630 
3631   /**
3632    * Undoable edit for the viewability modification of multiple levels.
3633    */
3634   private static class LevelsViewabilityModificationUndoableEdit extends LocalizedUndoableEdit {
3635     private final PlanController controller;
3636     private final Level          selectedLevel;
3637     private final boolean        selectedLevelViewable;
3638     private final Level []       viewableLevels;
3639 
3640     public LevelsViewabilityModificationUndoableEdit(PlanController controller, UserPreferences preferences,
3641                                                      Level selectedLevel, boolean selectedLevelViewable, Level [] viewableLevels) {
3642       super(preferences, PlanController.class, "undoModifyLevelViewabilityName");
3643       this.controller = controller;
3644       this.selectedLevel = selectedLevel;
3645       this.selectedLevelViewable = selectedLevelViewable;
3646       this.viewableLevels = viewableLevels;
3647     }
3648 
3649     @Override
3650     public void undo() throws CannotUndoException {
3651       super.undo();
3652       this.controller.setSelectedLevel(this.selectedLevel);
3653       setLevelsViewability(this.viewableLevels, true);
3654       this.selectedLevel.setViewable(this.selectedLevelViewable);
3655     }
3656 
3657     @Override
3658     public void redo() throws CannotRedoException {
3659       super.redo();
3660       this.controller.setSelectedLevel(this.selectedLevel);
3661       setLevelsViewability(this.viewableLevels, false);
3662       this.selectedLevel.setViewable(true);
3663     }
3664   }
3665 
3666   /**
3667    * Makes all levels viewable.
3668    * @since 6.0
3669    */
3670   public void setAllLevelsViewable() {
3671     final Level [] unviewableLevels = getLevels(false);
3672     if (unviewableLevels.length > 0) {
3673       final Level selectedLevel = this.home.getSelectedLevel();
3674       setLevelsViewability(unviewableLevels, true);
3675       undoSupport.postEdit(new AllLevelsViewabilityModificationUndoableEdit(this, this.preferences, selectedLevel, unviewableLevels));
3676     }
3677   }
3678 
3679   /**
3680    * Undoable edit for the viewability modification of all levels.
3681    */
3682   private static class AllLevelsViewabilityModificationUndoableEdit extends LocalizedUndoableEdit {
3683     private final PlanController controller;
3684     private final Level          selectedLevel;
3685     private final Level []       unviewableLevels;
3686 
3687     public AllLevelsViewabilityModificationUndoableEdit(PlanController controller, UserPreferences preferences,
3688                                                         Level selectedLevel, Level [] unviewableLevels) {
3689       super(preferences, PlanController.class, "undoModifyLevelViewabilityName");
3690       this.controller = controller;
3691       this.selectedLevel = selectedLevel;
3692       this.unviewableLevels = unviewableLevels;
3693     }
3694 
3695     @Override
3696     public void undo() throws CannotUndoException {
3697       super.undo();
3698       this.controller.setSelectedLevel(this.selectedLevel);
3699       setLevelsViewability(this.unviewableLevels, false);
3700     }
3701 
3702     @Override
3703     public void redo() throws CannotRedoException {
3704       super.redo();
3705       this.controller.setSelectedLevel(this.selectedLevel);
3706       setLevelsViewability(this.unviewableLevels, true);
3707     }
3708   }
3709 
3710   /**
3711    * Returns levels which are viewable or not according to parameter.
3712    */
3713   private Level [] getLevels(boolean viewable) {
3714     List<Level> levels = new ArrayList<Level>();
3715     for (Level level : this.home.getLevels()) {
3716       if (level.isViewable() == viewable) {
3717         levels.add(level);
3718       }
3719     }
3720     return levels.toArray(new Level [levels.size()]);
3721   }
3722 
3723   private static void setLevelsViewability(Level [] levels, boolean viewable) {
3724     for (Level level : levels) {
3725       level.setViewable(viewable);
3726     }
3727   }
3728 
3729   public void modifySelectedLevel() {
3730     if (this.home.getSelectedLevel() != null) {
3731       new LevelController(this.home, this.preferences, this.viewFactory,
3732           this.undoSupport).displayView(getView());
3733     }
3734   }
3735 
3736   /**
3737    * Deletes the selected level and the items that belongs to it.
3738    */
3739   public void deleteSelectedLevel() {
3740     // Start a compound edit that delete walls, furniture, rooms, dimension lines and labels from home
3741     undoSupport.beginUpdate();
3742     List<HomePieceOfFurniture> levelFurniture = new ArrayList<HomePieceOfFurniture>();
3743     final Level oldSelectedLevel = this.home.getSelectedLevel();
3744     for (HomePieceOfFurniture piece : this.home.getFurniture()) {
3745       if (piece.getLevel() == oldSelectedLevel) {
3746         levelFurniture.add(piece);
3747       }
3748     }
3749     // Delete furniture with inherited method
3750     deleteFurniture(levelFurniture);
3751 
3752     List<Selectable> levelOtherItems = new ArrayList<Selectable>();
3753     addLevelItemsAtSelectedLevel(this.home.getWalls(), levelOtherItems);
3754     addLevelItemsAtSelectedLevel(this.home.getRooms(), levelOtherItems);
3755     addLevelItemsAtSelectedLevel(this.home.getDimensionLines(), levelOtherItems);
3756     addLevelItemsAtSelectedLevel(this.home.getLabels(), levelOtherItems);
3757     // First post to undo support that walls, rooms and dimension lines are deleted,
3758     // otherwise data about joined walls and rooms index can't be stored
3759     postDeleteItems(levelOtherItems, this.home.isBasePlanLocked(), this.home.isAllLevelsSelection());
3760     // Then delete items from plan
3761     doDeleteItems(levelOtherItems);
3762 
3763     this.home.deleteLevel(oldSelectedLevel);
3764     List<Level> levels = this.home.getLevels();
3765     final Level remainingLevel;
3766     final Float remainingLevelElevation;
3767     final boolean remainingLevelViewable;
3768     if (levels.size() == 1) {
3769       remainingLevel = levels.get(0);
3770       remainingLevelElevation = remainingLevel.getElevation();
3771       remainingLevelViewable = remainingLevel.isViewable();
3772       remainingLevel.setElevation(0);
3773       remainingLevel.setViewable(true);
3774     } else {
3775       remainingLevel = null;
3776       remainingLevelElevation = null;
3777       remainingLevelViewable = false;
3778     }
3779     undoSupport.postEdit(new LevelDeletionUndoableEdit(this, this.home, this.preferences, oldSelectedLevel,
3780         remainingLevel, remainingLevelElevation, remainingLevelViewable));
3781 
3782     // End compound edit
3783     undoSupport.endUpdate();
3784   }
3785 
3786   /**
3787    * Undoable edit for the level deletion.
3788    */
3789   private static class LevelDeletionUndoableEdit extends LocalizedUndoableEdit {
3790     private final PlanController controller;
3791     private final Home           home;
3792     private final Level          oldSelectedLevel;
3793     private final Level          remainingLevel;
3794     private final Float          remainingLevelElevation;
3795     private final boolean        remainingLevelViewable;
3796 
3797     public LevelDeletionUndoableEdit(PlanController controller, Home home, UserPreferences preferences,
3798                                      Level oldSelectedLevel, Level remainingLevel,
3799                                      Float remainingLevelElevation, boolean remainingLevelViewable) {
3800       super(preferences, PlanController.class, "undoDeleteSelectedLevel");
3801       this.controller = controller;
3802       this.home = home;
3803       this.oldSelectedLevel = oldSelectedLevel;
3804       this.remainingLevel = remainingLevel;
3805       this.remainingLevelElevation = remainingLevelElevation;
3806       this.remainingLevelViewable = remainingLevelViewable;
3807     }
3808 
3809     @Override
3810     public void undo() throws CannotUndoException {
3811       super.undo();
3812       if (this.remainingLevel != null) {
3813         this.remainingLevel.setElevation(this.remainingLevelElevation);
3814         this.remainingLevel.setViewable(this.remainingLevelViewable);
3815       }
3816       this.home.addLevel(this.oldSelectedLevel);
3817       this.controller.setSelectedLevel(this.oldSelectedLevel);
3818     }
3819 
3820     @Override
3821     public void redo() throws CannotRedoException {
3822       super.redo();
3823       this.home.deleteLevel(this.oldSelectedLevel);
3824       if (this.remainingLevel != null) {
3825         this.remainingLevel.setElevation(0);
3826         this.remainingLevel.setViewable(true);
3827       }
3828     }
3829   }
3830 
3831   private void addLevelItemsAtSelectedLevel(Collection<? extends Selectable> items,
3832                                             List<Selectable> levelItems) {
3833     Level selectedLevel = this.home.getSelectedLevel();
3834     for (Selectable item : items) {
3835       if (item instanceof Elevatable
3836           && ((Elevatable)item).getLevel() == selectedLevel) {
3837         levelItems.add(item);
3838       }
3839     }
3840   }
3841 
3842   /**
3843    * Returns a new wall instance between (<code>xStart</code>,
3844    * <code>yStart</code>) and (<code>xEnd</code>, <code>yEnd</code>)
3845    * end points. The new wall is added to home and its start point is joined
3846    * to the start of <code>wallStartAtStart</code> or
3847    * the end of <code>wallEndAtStart</code>.
3848    */
3849   protected Wall createWall(float xStart, float yStart,
3850                             float xEnd, float yEnd,
3851                             Wall wallStartAtStart,
3852                             Wall wallEndAtStart) {
3853     // Create a new wall
3854     Wall newWall = new Wall(xStart, yStart, xEnd, yEnd,
3855         this.preferences.getNewWallThickness(),
3856         this.preferences.getNewWallHeight(),
3857         this.preferences.getNewWallPattern());
3858     this.home.addWall(newWall);
3859     if (wallStartAtStart != null) {
3860       newWall.setWallAtStart(wallStartAtStart);
3861       wallStartAtStart.setWallAtStart(newWall);
3862     } else if (wallEndAtStart != null) {
3863       newWall.setWallAtStart(wallEndAtStart);
3864       wallEndAtStart.setWallAtEnd(newWall);
3865     }
3866     return newWall;
3867   }
3868 
3869   /**
3870    * Joins the end point of <code>wall</code> to the start of
3871    * <code>wallStartAtEnd</code> or the end of <code>wallEndAtEnd</code>.
3872    */
3873   private void joinNewWallEndToWall(Wall wall,
3874                                     Wall wallStartAtEnd, Wall wallEndAtEnd) {
3875     if (wallStartAtEnd != null) {
3876       wall.setWallAtEnd(wallStartAtEnd);
3877       wallStartAtEnd.setWallAtStart(wall);
3878       // Make wall end at the exact same position as wallAtEnd start point
3879       wall.setXEnd(wallStartAtEnd.getXStart());
3880       wall.setYEnd(wallStartAtEnd.getYStart());
3881     } else if (wallEndAtEnd != null) {
3882       wall.setWallAtEnd(wallEndAtEnd);
3883       wallEndAtEnd.setWallAtEnd(wall);
3884       // Make wall end at the exact same position as wallAtEnd end point
3885       wall.setXEnd(wallEndAtEnd.getXEnd());
3886       wall.setYEnd(wallEndAtEnd.getYEnd());
3887     }
3888   }
3889 
3890   /**
3891    * Returns the wall at (<code>x</code>, <code>y</code>) point,
3892    * which has a start point not joined to any wall.
3893    */
3894   private Wall getWallStartAt(float x, float y, Wall ignoredWall) {
3895     float margin = WALL_ENDS_PIXEL_MARGIN / getScale();
3896     for (Wall wall : this.home.getWalls()) {
3897       if (wall != ignoredWall
3898           && isLevelNullOrViewable(wall.getLevel())
3899           && wall.isAtLevel(this.home.getSelectedLevel())
3900           && wall.getWallAtStart() == null
3901           && wall.containsWallStartAt(x, y, margin)) {
3902         return wall;
3903       }
3904     }
3905     return null;
3906   }
3907 
3908   /**
3909    * Returns the wall at (<code>x</code>, <code>y</code>) point,
3910    * which has a end point not joined to any wall.
3911    */
3912   private Wall getWallEndAt(float x, float y, Wall ignoredWall) {
3913     float margin = WALL_ENDS_PIXEL_MARGIN / getScale();
3914     for (Wall wall : this.home.getWalls()) {
3915       if (wall != ignoredWall
3916           && isLevelNullOrViewable(wall.getLevel())
3917           && wall.isAtLevel(this.home.getSelectedLevel())
3918           && wall.getWallAtEnd() == null
3919           && wall.containsWallEndAt(x, y, margin)) {
3920         return wall;
3921       }
3922     }
3923     return null;
3924   }
3925 
3926   /**
3927    * Returns the tolerance margin to handle an indicator.
3928    */
3929   private float getIndicatorMargin() {
3930     float indicatorPixelMagin = INDICATOR_PIXEL_MARGIN;
3931     if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
3932       indicatorPixelMagin *= 3;
3933     }
3934     return indicatorPixelMagin / getScale();
3935   }
3936 
3937   /**
3938    * Returns the selected wall with a start point
3939    * at (<code>x</code>, <code>y</code>).
3940    */
3941   private Wall getResizedWallStartAt(float x, float y) {
3942     List<Selectable> selectedItems = this.home.getSelectedItems();
3943     if (selectedItems.size() == 1
3944         && selectedItems.get(0) instanceof Wall
3945         && isItemResizable(selectedItems.get(0))) {
3946       Wall wall = (Wall)selectedItems.get(0);
3947       float margin = getIndicatorMargin();
3948       if (wall.isAtLevel(this.home.getSelectedLevel())
3949           && wall.containsWallStartAt(x, y, margin)) {
3950         return wall;
3951       }
3952     }
3953     return null;
3954   }
3955 
3956   /**
3957    * Returns the selected wall with an end point at (<code>x</code>, <code>y</code>).
3958    */
3959   private Wall getResizedWallEndAt(float x, float y) {
3960     List<Selectable> selectedItems = this.home.getSelectedItems();
3961     if (selectedItems.size() == 1
3962         && selectedItems.get(0) instanceof Wall
3963         && isItemResizable(selectedItems.get(0))) {
3964       Wall wall = (Wall)selectedItems.get(0);
3965       float margin = getIndicatorMargin();
3966       if (wall.isAtLevel(this.home.getSelectedLevel())
3967           && wall.containsWallEndAt(x, y, margin)) {
3968         return wall;
3969       }
3970     }
3971     return null;
3972   }
3973 
3974   /**
3975    * Returns the selected wall with a middle point at (<code>x</code>, <code>y</code>).
3976    */
3977   private Wall getArcExtentWallAt(float x, float y) {
3978     List<Selectable> selectedItems = this.home.getSelectedItems();
3979     if (selectedItems.size() == 1
3980         && selectedItems.get(0) instanceof Wall
3981         && isItemResizable(selectedItems.get(0))) {
3982       Wall wall = (Wall)selectedItems.get(0);
3983       float margin = getIndicatorMargin();
3984       if (wall.isAtLevel(this.home.getSelectedLevel())
3985           && wall.isMiddlePointAt(x, y, margin)) {
3986         return wall;
3987       }
3988     }
3989     return null;
3990   }
3991 
3992   /**
3993    * Returns a new room instance with the given points.
3994    * The new room is added to home.
3995    */
3996   protected Room createRoom(float [][] roomPoints) {
3997     Room newRoom = new Room(roomPoints);
3998     newRoom.setFloorColor(this.preferences.getNewRoomFloorColor());
3999     this.home.addRoom(newRoom);
4000     return newRoom;
4001   }
4002 
4003   /**
4004    * Returns the selected room with a point at (<code>x</code>, <code>y</code>).
4005    */
4006   private Room getResizedRoomAt(float x, float y) {
4007     List<Selectable> selectedItems = this.home.getSelectedItems();
4008     if (selectedItems.size() == 1
4009         && selectedItems.get(0) instanceof Room
4010         && isItemResizable(selectedItems.get(0))) {
4011       Room room = (Room)selectedItems.get(0);
4012       float margin = getIndicatorMargin();
4013       if (room.isAtLevel(this.home.getSelectedLevel())
4014           && room.getPointIndexAt(x, y, margin) != -1) {
4015         return room;
4016       }
4017     }
4018     return null;
4019   }
4020 
4021   /**
4022    * Returns the selected room with its name center point at (<code>x</code>, <code>y</code>).
4023    */
4024   private Room getRoomNameAt(float x, float y) {
4025     List<Selectable> selectedItems = this.home.getSelectedItems();
4026     if (selectedItems.size() == 1
4027         && selectedItems.get(0) instanceof Room
4028         && isItemMovable(selectedItems.get(0))) {
4029       Room room = (Room)selectedItems.get(0);
4030       float margin = getIndicatorMargin();
4031       if (room.isAtLevel(this.home.getSelectedLevel())
4032           && room.getName() != null
4033           && room.getName().trim().length() > 0
4034           && room.isNameCenterPointAt(x, y, margin)) {
4035         return room;
4036       }
4037     }
4038     return null;
4039   }
4040 
4041   /**
4042    * Returns the selected room with its
4043    * name angle point at (<code>x</code>, <code>y</code>).
4044    */
4045   private Room getRoomRotatedNameAt(float x, float y) {
4046     List<Selectable> selectedItems = this.home.getSelectedItems();
4047     if (selectedItems.size() == 1
4048         && selectedItems.get(0) instanceof Room
4049         && isItemMovable(selectedItems.get(0))) {
4050       Room room = (Room)selectedItems.get(0);
4051       float margin = getIndicatorMargin();
4052       if (room.isAtLevel(this.home.getSelectedLevel())
4053           && room.getName() != null
4054           && room.getName().trim().length() > 0
4055           && isTextAnglePointAt(room, room.getName(), room.getNameStyle(),
4056               room.getXCenter() + room.getNameXOffset(), room.getYCenter() + room.getNameYOffset(),
4057               room.getNameAngle(), x, y, margin)) {
4058         return room;
4059       }
4060     }
4061     return null;
4062   }
4063 
4064   /**
4065    * Returns the selected room with its area center point at (<code>x</code>, <code>y</code>).
4066    */
4067   private Room getRoomAreaAt(float x, float y) {
4068     List<Selectable> selectedItems = this.home.getSelectedItems();
4069     if (selectedItems.size() == 1
4070         && selectedItems.get(0) instanceof Room
4071         && isItemMovable(selectedItems.get(0))) {
4072       Room room = (Room)selectedItems.get(0);
4073       float margin = getIndicatorMargin();
4074       if (room.isAtLevel(this.home.getSelectedLevel())
4075           && room.isAreaVisible()
4076           && room.isAreaCenterPointAt(x, y, margin)) {
4077         return room;
4078       }
4079     }
4080     return null;
4081   }
4082 
4083   /**
4084    * Returns the selected room with its
4085    * area angle point at (<code>x</code>, <code>y</code>).
4086    */
4087   private Room getRoomRotatedAreaAt(float x, float y) {
4088     List<Selectable> selectedItems = this.home.getSelectedItems();
4089     if (selectedItems.size() == 1
4090         && selectedItems.get(0) instanceof Room
4091         && isItemMovable(selectedItems.get(0))) {
4092       Room room = (Room)selectedItems.get(0);
4093       float margin = getIndicatorMargin();
4094       if (room.isAtLevel(this.home.getSelectedLevel())
4095           && room.isAreaVisible()) {
4096         float area = room.getArea();
4097         if (area > 0.01f) {
4098           String areaText = this.preferences.getLengthUnit().getAreaFormatWithUnit().format(area);
4099           if (isTextAnglePointAt(room, areaText, room.getAreaStyle(),
4100                   room.getXCenter() + room.getAreaXOffset(), room.getYCenter() + room.getAreaYOffset(),
4101                   room.getAreaAngle(), x, y, margin)) {
4102             return room;
4103           }
4104         }
4105       }
4106     }
4107     return null;
4108   }
4109 
4110   /**
4111    * Adds a point to the selected room at the given coordinates and posts an undoable operation.
4112    * @since 5.0
4113    */
4114   public void addPointToSelectedRoom(final float x, final float y) {
4115     final List<Selectable> selectedItems = this.home.getSelectedItems();
4116     if (selectedItems.size() == 1
4117         && selectedItems.get(0) instanceof Room
4118         && isItemResizable(selectedItems.get(0))) {
4119       final Room room = (Room)selectedItems.get(0);
4120       final float [][] points = room.getPoints();
4121       // Search the segment closest to (x, y)
4122       int closestSegmentIndex = -1;
4123       double smallestDistance = Double.MAX_VALUE;
4124       for (int i = 0; i < points.length; i++) {
4125         float [] point = points [i];
4126         float [] nextPoint = points [(i + 1) % points.length];
4127         double distanceToSegment = Line2D.ptSegDistSq(point [0], point [1], nextPoint [0], nextPoint [1], x, y);
4128         if (smallestDistance > distanceToSegment) {
4129           smallestDistance = distanceToSegment;
4130           closestSegmentIndex = i;
4131         }
4132       }
4133       final int index = closestSegmentIndex + 1;
4134       room.addPoint(x, y, index);
4135       this.home.setSelectedItems(Arrays.asList(new Room [] {room}));
4136       // Upright an undoable edit
4137       this.undoSupport.postEdit(new RoomPointAdditionUndoableEdit(this, this.preferences,
4138           selectedItems.toArray(new Selectable [selectedItems.size()]), room, index, x, y));
4139     }
4140   }
4141 
4142   /**
4143    * Undoable edit for room point addition.
4144    */
4145   private static class RoomPointAdditionUndoableEdit extends LocalizedUndoableEdit {
4146     private final PlanController controller;
4147     private final Selectable []  oldSelection;
4148     private final Room           room;
4149     private final int            index;
4150     private final float          x;
4151     private final float          y;
4152 
4153     public RoomPointAdditionUndoableEdit(PlanController controller, UserPreferences preferences,
4154                                          Selectable [] oldSelection, Room room, int index, float x, float y) {
4155       super(preferences, PlanController.class, "undoAddRoomPointName");
4156       this.controller = controller;
4157       this.oldSelection = oldSelection;
4158       this.room = room;
4159       this.index = index;
4160       this.x = x;
4161       this.y = y;
4162     }
4163 
4164     @Override
4165     public void undo() throws CannotUndoException {
4166       super.undo();
4167       this.room.removePoint(this.index);
4168       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection));
4169     }
4170 
4171     @Override
4172     public void redo() throws CannotRedoException {
4173       super.redo();
4174       this.room.addPoint(this.x, this.y, this.index);
4175       this.controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
4176     }
4177   }
4178 
4179   /**
4180    * Returns <code>true</code> if the given point can be removed from the <code>room</code>.
4181    */
4182   public boolean isRoomPointDeletableAt(Room room, float x, float y) {
4183     return isItemResizable(room)
4184         && room.getPointIndexAt(x, y, getIndicatorMargin()) >= 0;
4185   }
4186 
4187   /**
4188    * Deletes the point of the selected room at the given coordinates and posts an undoable operation.
4189    * @since 5.0
4190    */
4191   public void deletePointFromSelectedRoom(float x, float y) {
4192     final List<Selectable> selectedItems = this.home.getSelectedItems();
4193     if (selectedItems.size() == 1
4194         && selectedItems.get(0) instanceof Room
4195         && isItemResizable(selectedItems.get(0))) {
4196       final Room room = (Room)selectedItems.get(0);
4197       final int index = room.getPointIndexAt(x, y, getIndicatorMargin());
4198       if (index >= 0) {
4199         float [][] points = room.getPoints();
4200         float [] point = points [index];
4201         final float xPoint = point [0];
4202         final float yPoint = point [1];
4203 
4204         room.removePoint(index);
4205         this.home.setSelectedItems(Arrays.asList(new Room [] {room}));
4206         // Upright an undoable edit
4207         this.undoSupport.postEdit(new RoomPointDeletionUndoableEdit(this, this.preferences,
4208             selectedItems.toArray(new Selectable [selectedItems.size()]), room, index, xPoint, yPoint));
4209       }
4210     }
4211   }
4212 
4213   /**
4214    * Undoable edit for room point deletion.
4215    */
4216   private static class RoomPointDeletionUndoableEdit extends LocalizedUndoableEdit {
4217     private final PlanController controller;
4218     private final Selectable []  oldSelection;
4219     private final Room           room;
4220     private final int            index;
4221     private final float          xPoint;
4222     private final float          yPoint;
4223 
4224     public RoomPointDeletionUndoableEdit(PlanController controller, UserPreferences preferences,
4225                                          Selectable [] oldSelection, Room room, int index,
4226                                          float xPoint, float yPoint) {
4227       super(preferences, PlanController.class, "undoDeleteRoomPointName");
4228       this.controller = controller;
4229       this.oldSelection = oldSelection;
4230       this.room = room;
4231       this.index = index;
4232       this.xPoint = xPoint;
4233       this.yPoint = yPoint;
4234     }
4235 
4236     @Override
4237     public void undo() throws CannotUndoException {
4238       super.undo();
4239       this.room.addPoint(this.xPoint, this.yPoint, this.index);
4240       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection));
4241     }
4242 
4243     @Override
4244     public void redo() throws CannotRedoException {
4245       super.redo();
4246       this.room.removePoint(this.index);
4247       this.controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
4248     }
4249   }
4250 
4251   /**
4252    * Returns a new dimension instance joining (<code>xStart</code>,
4253    * <code>yStart</code>) and (<code>xEnd</code>, <code>yEnd</code>) points.
4254    * The new dimension line is added to home.
4255    */
4256   protected DimensionLine createDimensionLine(float xStart, float yStart,
4257                                               float xEnd, float yEnd,
4258                                               float offset) {
4259     DimensionLine newDimensionLine = new DimensionLine(xStart, yStart, xEnd, yEnd, offset);
4260     this.home.addDimensionLine(newDimensionLine);
4261     return newDimensionLine;
4262   }
4263 
4264   /**
4265    * Returns the selected dimension line with an end extension line
4266    * at (<code>x</code>, <code>y</code>).
4267    */
4268   private DimensionLine getResizedDimensionLineStartAt(float x, float y) {
4269     List<Selectable> selectedItems = this.home.getSelectedItems();
4270     if (selectedItems.size() == 1
4271         && selectedItems.get(0) instanceof DimensionLine
4272         && isItemResizable(selectedItems.get(0))) {
4273       DimensionLine dimensionLine = (DimensionLine)selectedItems.get(0);
4274       float margin = getIndicatorMargin();
4275       if (dimensionLine.isAtLevel(this.home.getSelectedLevel())
4276           && dimensionLine.containsStartExtensionLinetAt(x, y, margin)) {
4277         return dimensionLine;
4278       }
4279     }
4280     return null;
4281   }
4282 
4283   /**
4284    * Returns the selected dimension line with an end extension line
4285    * at (<code>x</code>, <code>y</code>).
4286    */
4287   private DimensionLine getResizedDimensionLineEndAt(float x, float y) {
4288     List<Selectable> selectedItems = this.home.getSelectedItems();
4289     if (selectedItems.size() == 1
4290         && selectedItems.get(0) instanceof DimensionLine
4291         && isItemResizable(selectedItems.get(0))) {
4292       DimensionLine dimensionLine = (DimensionLine)selectedItems.get(0);
4293       float margin = getIndicatorMargin();
4294       if (dimensionLine.isAtLevel(this.home.getSelectedLevel())
4295           && dimensionLine.containsEndExtensionLineAt(x, y, margin)) {
4296         return dimensionLine;
4297       }
4298     }
4299     return null;
4300   }
4301 
4302   /**
4303    * Returns the selected dimension line with a point
4304    * at (<code>x</code>, <code>y</code>) at its middle.
4305    */
4306   private DimensionLine getOffsetDimensionLineAt(float x, float y) {
4307     List<Selectable> selectedItems = this.home.getSelectedItems();
4308     if (selectedItems.size() == 1
4309         && selectedItems.get(0) instanceof DimensionLine
4310         && isItemResizable(selectedItems.get(0))) {
4311       DimensionLine dimensionLine = (DimensionLine)selectedItems.get(0);
4312       float margin = getIndicatorMargin();
4313       if (dimensionLine.isAtLevel(this.home.getSelectedLevel())
4314           && dimensionLine.isMiddlePointAt(x, y, margin)) {
4315         return dimensionLine;
4316       }
4317     }
4318     return null;
4319   }
4320 
4321   /**
4322    * Returns a new polyline instance with the given points.
4323    * The new polyline is added to home.
4324    */
4325   private Polyline createPolyline(float [][] polylinePoints) {
4326     Polyline newPolyline = new Polyline(polylinePoints);
4327     LengthUnit lengthUnit = preferences.getLengthUnit();
4328     newPolyline.setThickness(lengthUnit == LengthUnit.INCH || lengthUnit == LengthUnit.INCH_DECIMALS
4329         ? LengthUnit.inchToCentimeter(1)
4330         : 2);
4331     this.home.addPolyline(newPolyline);
4332     return newPolyline;
4333   }
4334 
4335   /**
4336    * Returns the selected polyline with a point at (<code>x</code>, <code>y</code>).
4337    */
4338   private Polyline getResizedPolylineAt(float x, float y) {
4339     List<Selectable> selectedItems = this.home.getSelectedItems();
4340     if (selectedItems.size() == 1
4341         && selectedItems.get(0) instanceof Polyline
4342         && isItemResizable(selectedItems.get(0))) {
4343       Polyline polyline = (Polyline)selectedItems.get(0);
4344       float margin = getIndicatorMargin();
4345       if (polyline.isAtLevel(this.home.getSelectedLevel())
4346           && polyline.getPointIndexAt(x, y, margin) != -1) {
4347         return polyline;
4348       }
4349     }
4350     return null;
4351   }
4352 
4353   /**
4354    * Returns the tolerance margin to select an item.
4355    */
4356   private float getSelectionMargin() {
4357     float indicatorPixelMagin = PIXEL_MARGIN;
4358     if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
4359       indicatorPixelMagin *= 2;
4360     }
4361     return indicatorPixelMagin / getScale();
4362   }
4363 
4364   /**
4365    * Returns the selected item at (<code>x</code>, <code>y</code>) point.
4366    */
4367   private boolean isItemSelectedAt(float x, float y) {
4368     float margin = getSelectionMargin();
4369     for (Selectable item : this.home.getSelectedItems()) {
4370       if (item.containsPoint(x, y, margin)) {
4371         return true;
4372       }
4373     }
4374     return false;
4375   }
4376 
4377   /**
4378    * Returns the selectable item at (<code>x</code>, <code>y</code>) point.
4379    */
4380   public Selectable getSelectableItemAt(float x, float y) {
4381     return getSelectableItemAt(x, y, true);
4382   }
4383 
4384   /**
4385    * Returns the selectable item at (<code>x</code>, <code>y</code>) point.
4386    */
4387   private Selectable getSelectableItemAt(float x, float y, boolean ignoreGroupsFurniture) {
4388     List<Selectable> selectableItems = getSelectableItemsAt(x, y, true, ignoreGroupsFurniture);
4389     if (selectableItems.size() != 0) {
4390       return selectableItems.get(0);
4391     } else {
4392       return null;
4393     }
4394   }
4395 
4396   /**
4397    * Returns the selectable items at (<code>x</code>, <code>y</code>) point.
4398    */
4399   public List<Selectable> getSelectableItemsAt(float x, float y) {
4400     return getSelectableItemsAt(x, y, false, true);
4401   }
4402 
4403   /**
4404    * Returns the selectable items at (<code>x</code>, <code>y</code>) point.
4405    */
4406   private List<Selectable> getSelectableItemsAt(float x, float y,
4407                                                 boolean stopAtFirstItem,
4408                                                 boolean ignoreGroupsFurniture) {
4409     List<Selectable> items = new ArrayList<Selectable>();
4410     float margin = getSelectionMargin();
4411     float textMargin = margin / 2;
4412     ObserverCamera camera = this.home.getObserverCamera();
4413     if (camera != null
4414         && camera == this.home.getCamera()
4415         && camera.containsPoint(x, y, margin)) {
4416       items.add(camera);
4417       if (stopAtFirstItem) {
4418         return items;
4419       }
4420     }
4421 
4422     boolean basePlanLocked = this.home.isBasePlanLocked();
4423     Level selectedLevel = this.home.getSelectedLevel();
4424     for (Label label : this.home.getLabels()) {
4425       if ((!basePlanLocked
4426             || !isItemPartOfBasePlan(label))
4427           && isLevelNullOrViewable(label.getLevel())
4428           && label.isAtLevel(selectedLevel)
4429           && (label.containsPoint(x, y, margin)
4430               || isItemTextAt(label, label.getText(), label.getStyle(),
4431                     label.getX(), label.getY(), label.getAngle(), x, y, textMargin))) {
4432         items.add(label);
4433         if (stopAtFirstItem) {
4434           return items;
4435         }
4436       }
4437     }
4438 
4439     for (DimensionLine dimensionLine : this.home.getDimensionLines()) {
4440       if ((!basePlanLocked
4441             || !isItemPartOfBasePlan(dimensionLine))
4442           && isLevelNullOrViewable(dimensionLine.getLevel())
4443           && dimensionLine.isAtLevel(selectedLevel)
4444           && dimensionLine.containsPoint(x, y, margin)) {
4445         items.add(dimensionLine);
4446         if (stopAtFirstItem) {
4447           return items;
4448         }
4449       }
4450     }
4451 
4452     List<Polyline> polylines = this.home.getPolylines();
4453     // Search in home polylines in reverse order to give priority to last drawn polyline
4454     for (int i = polylines.size() - 1; i >= 0; i--) {
4455       Polyline polyline = polylines.get(i);
4456       if ((!basePlanLocked
4457             || !isItemPartOfBasePlan(polyline))
4458           && isLevelNullOrViewable(polyline.getLevel())
4459           && polyline.isAtLevel(selectedLevel)
4460           && polyline.containsPoint(x, y, margin)) {
4461         items.add(polyline);
4462         if (stopAtFirstItem) {
4463           return items;
4464         }
4465       }
4466     }
4467 
4468     List<HomePieceOfFurniture> furniture = this.home.getFurniture();
4469     // Search in home furniture in reverse order to give priority to last drawn piece
4470     // at highest elevation in case it covers an other piece
4471     List<HomePieceOfFurniture> foundFurniture = new ArrayList<HomePieceOfFurniture>();
4472     HomePieceOfFurniture foundPiece = null;
4473     for (int i = furniture.size() - 1; i >= 0; i--) {
4474       HomePieceOfFurniture piece = furniture.get(i);
4475       if ((!basePlanLocked
4476             || !isItemPartOfBasePlan(piece))
4477           && isPieceOfFurnitureVisibleAtSelectedLevel(piece)) {
4478         if (piece.containsPoint(x, y, margin)) {
4479           foundFurniture.add(piece);
4480           if (foundPiece == null
4481               || piece.getGroundElevation() > foundPiece.getGroundElevation()) {
4482             foundPiece = piece;
4483           }
4484         } else if (foundPiece == null) {
4485           // Search if piece name contains point in case it is drawn outside of the piece
4486           String pieceName = piece.getName();
4487           if (pieceName != null
4488               && piece.isNameVisible()
4489               && isItemTextAt(piece, pieceName, piece.getNameStyle(),
4490                   piece.getX() + piece.getNameXOffset(),
4491                   piece.getY() + piece.getNameYOffset(), piece.getNameAngle(), x, y, textMargin)) {
4492             foundFurniture.add(piece);
4493             foundPiece = piece;
4494           }
4495         }
4496       }
4497     }
4498     if (foundPiece == null
4499         && basePlanLocked) {
4500       // Check among the furniture that is already selected if there's a movable piece at the given location
4501       for (Selectable item : home.getSelectedItems()) {
4502         if (item instanceof HomePieceOfFurniture) {
4503           HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
4504           if (!isItemPartOfBasePlan(piece)
4505               && isPieceOfFurnitureVisibleAtSelectedLevel(piece)
4506               && (piece.containsPoint(x, y, margin)
4507                   || piece.getName() != null
4508                       && piece.isNameVisible()
4509                       && isItemTextAt(piece, piece.getName(), piece.getNameStyle(),
4510                           piece.getX() + piece.getNameXOffset(),
4511                           piece.getY() + piece.getNameYOffset(), piece.getNameAngle(), x, y, textMargin))) {
4512             foundFurniture.add(piece);
4513             foundPiece = piece;
4514             if (stopAtFirstItem) {
4515               break;
4516             }
4517           }
4518         }
4519       }
4520     }
4521     if (foundPiece != null
4522         && stopAtFirstItem) {
4523       if (!ignoreGroupsFurniture
4524           && (foundPiece instanceof HomeFurnitureGroup)) {
4525         List<Selectable> selectedItems = this.home.getSelectedItems();
4526         if (selectedItems.size() >= 1) {
4527           // If selected items are in the same group
4528           if ((selectedItems.size() == 1 && selectedItems.get(0) == foundPiece)
4529               || ((HomeFurnitureGroup)foundPiece).getAllFurniture().containsAll(selectedItems)) {
4530             for (Selectable selectedItem : selectedItems) {
4531               if (selectedItem instanceof HomeFurnitureGroup) {
4532                 // Search the piece at point among the furniture of the selected group
4533                 List<HomePieceOfFurniture> groupFurniture = ((HomeFurnitureGroup)selectedItem).getFurniture();
4534                 for (int i = groupFurniture.size() - 1; i >= 0; i--) {
4535                   HomePieceOfFurniture piece = groupFurniture.get(i);
4536                   if ((!basePlanLocked
4537                         || !isItemPartOfBasePlan(piece))
4538                       && !selectedItems.contains(piece)
4539                       && piece.containsPoint(x, y, margin)) {
4540                     return Arrays.asList(new Selectable [] {piece});
4541                   }
4542                 }
4543               }
4544             }
4545             // Search the piece at point among the groups of selected furniture
4546             for (Selectable selectedItem : selectedItems) {
4547               if (selectedItem instanceof HomePieceOfFurniture) {
4548                 List<HomePieceOfFurniture> groupFurniture = getFurnitureInSameGroup((HomePieceOfFurniture)selectedItem);
4549                 for (int i = groupFurniture.size() - 1; i >= 0; i--) {
4550                   HomePieceOfFurniture piece = groupFurniture.get(i);
4551                   if ((!basePlanLocked
4552                         || !isItemPartOfBasePlan(piece))
4553                       && piece.containsPoint(x, y, margin)) {
4554                     return Arrays.asList(new Selectable [] {piece});
4555                   }
4556                 }
4557               }
4558             }
4559           }
4560         }
4561       }
4562       return Arrays.asList(new Selectable [] {foundPiece});
4563     } else {
4564       Collections.sort(foundFurniture, new Comparator<HomePieceOfFurniture>() {
4565           public int compare(HomePieceOfFurniture p1, HomePieceOfFurniture p2) {
4566             return -Float.compare(p1.getGroundElevation(), p2.getGroundElevation());
4567           }
4568         });
4569       items.addAll(foundFurniture);
4570       for (Wall wall : this.home.getWalls()) {
4571         if ((!basePlanLocked
4572               || !isItemPartOfBasePlan(wall))
4573             && isLevelNullOrViewable(wall.getLevel())
4574             && wall.isAtLevel(selectedLevel)
4575             && wall.containsPoint(x, y, margin)) {
4576           items.add(wall);
4577           if (stopAtFirstItem) {
4578             return items;
4579           }
4580         }
4581       }
4582 
4583       List<Room> rooms = this.home.getRooms();
4584       // Search in home rooms in reverse order to give priority to last drawn room
4585       // at highest elevation in case it covers an other piece
4586       Room foundRoom = null;
4587       for (int i = rooms.size() - 1; i >= 0; i--) {
4588         Room room = rooms.get(i);
4589         if ((!basePlanLocked
4590               || !isItemPartOfBasePlan(room))
4591             && isLevelNullOrViewable(room.getLevel())
4592             && room.isAtLevel(selectedLevel)) {
4593           if (room.containsPoint(x, y, margin)) {
4594             items.add(room);
4595              if (foundRoom == null
4596                  || room.isCeilingVisible() && !foundRoom.isCeilingVisible()) {
4597                foundRoom = room;
4598              }
4599           } else {
4600             // Search if room name contains point in case it is drawn outside of the room
4601             String roomName = room.getName();
4602             if (roomName != null
4603                 && isItemTextAt(room, roomName, room.getNameStyle(),
4604                   room.getXCenter() + room.getNameXOffset(),
4605                   room.getYCenter() + room.getNameYOffset(), room.getNameAngle(), x, y, textMargin)) {
4606               items.add(room);
4607               foundRoom = room;
4608             }
4609             // Search if room area contains point in case its text is drawn outside of the room
4610             if (room.isAreaVisible()) {
4611               String areaText = this.preferences.getLengthUnit().getAreaFormatWithUnit().format(room.getArea());
4612               if (isItemTextAt(room, areaText, room.getAreaStyle(),
4613                   room.getXCenter() + room.getAreaXOffset(),
4614                   room.getYCenter() + room.getAreaYOffset(), room.getAreaAngle(), x, y, textMargin)) {
4615                 items.add(room);
4616                 foundRoom = room;
4617               }
4618             }
4619           }
4620         }
4621       }
4622       if (foundRoom != null
4623           && stopAtFirstItem) {
4624         return Arrays.asList(new Selectable [] {foundRoom});
4625       } else {
4626         Compass compass = this.home.getCompass();
4627         if ((!basePlanLocked
4628               || !isItemPartOfBasePlan(compass))
4629             && compass.containsPoint(x, y, textMargin)) {
4630           items.add(compass);
4631         }
4632         return items;
4633       }
4634     }
4635   }
4636 
4637   /**
4638    * Returns <code>true</code> if the <code>text</code> of an <code>item</code> displayed
4639    * at the point (<code>xText</code>, <code>yText</code>) contains the point (<code>x</code>, <code>y</code>).
4640    */
4641   private boolean isItemTextAt(Selectable item, String text, TextStyle textStyle, float xText, float yText, float textAngle,
4642                                float x, float y, float textMargin) {
4643     if (textStyle == null) {
4644       textStyle = this.preferences.getDefaultTextStyle(item.getClass());
4645     }
4646     float [][] textBounds = getView().getTextBounds(text, textStyle, xText, yText, textAngle);
4647     return getPath(textBounds).intersects(x - textMargin, y - textMargin, 2 * textMargin, 2 * textMargin);
4648   }
4649 
4650   /**
4651    * Returns the items that intersects with the rectangle of (<code>x0</code>,
4652    * <code>y0</code>), (<code>x1</code>, <code>y1</code>) opposite corners.
4653    */
4654   protected List<Selectable> getSelectableItemsIntersectingRectangle(float x0, float y0, float x1, float y1) {
4655     List<Selectable> items = new ArrayList<Selectable>();
4656     boolean basePlanLocked = this.home.isBasePlanLocked();
4657     for (Selectable item : getVisibleItemsAtSelectedLevel()) {
4658       if ((!basePlanLocked
4659             || !isItemPartOfBasePlan(item))
4660           && item.intersectsRectangle(x0, y0, x1, y1)) {
4661         items.add(item);
4662       }
4663     }
4664     ObserverCamera camera = this.home.getObserverCamera();
4665     if (camera != null && camera.intersectsRectangle(x0, y0, x1, y1)) {
4666       items.add(camera);
4667     }
4668     return items;
4669   }
4670 
4671   /**
4672    * Returns the selected piece of furniture with a point
4673    * at (<code>x</code>, <code>y</code>) that can be used to rotate the piece.
4674    */
4675   private HomePieceOfFurniture getRotatedPieceOfFurnitureAt(float x, float y) {
4676     HomePieceOfFurniture selectedPiece = getSelectedMovablePieceOfFurniture();
4677     if (selectedPiece != null) {
4678       float margin = getIndicatorMargin();
4679       if (selectedPiece.isTopLeftPointAt(x, y, margin)
4680           // Ignore piece shape to ensure there's always enough space to drag it
4681           && !selectedPiece.containsPoint(x, y, 0)) {
4682         return selectedPiece;
4683       }
4684     }
4685     return null;
4686   }
4687 
4688   /**
4689    * Returns the selected piece of furniture with a point
4690    * at (<code>x</code>, <code>y</code>) that can be used to elevate the piece.
4691    */
4692   private HomePieceOfFurniture getElevatedPieceOfFurnitureAt(float x, float y) {
4693     HomePieceOfFurniture selectedPiece = getSelectedMovablePieceOfFurniture();
4694     if (selectedPiece != null) {
4695       float margin = getIndicatorMargin();
4696       if (selectedPiece.isTopRightPointAt(x, y, margin)
4697           // Ignore piece shape to ensure there's always enough space to drag it
4698           && !selectedPiece.containsPoint(x, y, 0)) {
4699         return selectedPiece;
4700       }
4701     }
4702     return null;
4703   }
4704 
4705   /**
4706    * Returns the selected piece of furniture with a point
4707    * at (<code>x</code>, <code>y</code>) that can be used to resize the height
4708    * of the piece.
4709    */
4710   private HomePieceOfFurniture getHeightResizedPieceOfFurnitureAt(float x, float y) {
4711     HomePieceOfFurniture selectedPiece = getSelectedResizablePieceOfFurniture();
4712     if (selectedPiece != null) {
4713       float margin = getIndicatorMargin();
4714       if (!selectedPiece.isHorizontallyRotated()
4715           && selectedPiece.isBottomLeftPointAt(x, y, margin)
4716           // Ignore piece shape to ensure there's always enough space to drag it
4717           && !selectedPiece.containsPoint(x, y, 0)) {
4718         return selectedPiece;
4719       }
4720     }
4721     return null;
4722   }
4723 
4724   /**
4725    * Returns the selected piece of furniture with a point
4726    * at (<code>x</code>, <code>y</code>) that can be used to rotate the piece
4727    * around the pitch axis.
4728    */
4729   private HomePieceOfFurniture getPitchRotatedPieceOfFurnitureAt(float x, float y) {
4730     HomePieceOfFurniture selectedPiece = getSelectedMovablePieceOfFurniture();
4731     if (selectedPiece != null
4732         && this.getView().isFurnitureSizeInPlanSupported()) {
4733       float margin = getIndicatorMargin();
4734       if (selectedPiece.getPitch() != 0
4735           && selectedPiece.isBottomLeftPointAt(x, y, margin)
4736           // Ignore piece shape to ensure there's always enough space to drag it
4737           && !selectedPiece.containsPoint(x, y, 0)) {
4738         return selectedPiece;
4739       }
4740     }
4741     return null;
4742   }
4743 
4744   /**
4745    * Returns the selected piece of furniture with a point
4746    * at (<code>x</code>, <code>y</code>) that can be used to rotate the piece
4747    * around the roll axis.
4748    */
4749   private HomePieceOfFurniture getRollRotatedPieceOfFurnitureAt(float x, float y) {
4750     HomePieceOfFurniture selectedPiece = getSelectedMovablePieceOfFurniture();
4751     if (selectedPiece != null
4752         && this.getView().isFurnitureSizeInPlanSupported()) {
4753       float margin = getIndicatorMargin();
4754       if (selectedPiece.getRoll() != 0
4755           && selectedPiece.isBottomLeftPointAt(x, y, margin)
4756           // Ignore piece shape to ensure there's always enough space to drag it
4757           && !selectedPiece.containsPoint(x, y, 0)) {
4758         return selectedPiece;
4759       }
4760     }
4761     return null;
4762   }
4763 
4764   /**
4765    * Returns the selected piece of furniture with a point
4766    * at (<code>x</code>, <code>y</code>) that can be used to resize
4767    * the width and the depth of the piece.
4768    */
4769   private HomePieceOfFurniture getWidthAndDepthResizedPieceOfFurnitureAt(float x, float y) {
4770     HomePieceOfFurniture selectedPiece = getSelectedResizablePieceOfFurniture();
4771     if (selectedPiece != null) {
4772       float margin = getIndicatorMargin();
4773       if (selectedPiece.isBottomRightPointAt(x, y, margin)
4774           // Ignore piece shape to ensure there's always enough space to drag it
4775           && !selectedPiece.containsPoint(x, y, 0)) {
4776         return selectedPiece;
4777       }
4778     }
4779     return null;
4780   }
4781 
4782   /**
4783    * Returns the selected item if selection contains one selected movable piece of furniture.
4784    */
4785   private HomePieceOfFurniture getSelectedMovablePieceOfFurniture() {
4786     HomePieceOfFurniture selectedPiece = getSelectedPieceOfFurniture();
4787     if (selectedPiece != null
4788         && isItemMovable(selectedPiece)) {
4789       return selectedPiece;
4790     }
4791     return null;
4792   }
4793 
4794   /**
4795    * Returns the selected item if selection contains one selected resizable piece of furniture.
4796    */
4797   private HomePieceOfFurniture getSelectedResizablePieceOfFurniture() {
4798     HomePieceOfFurniture selectedPiece = getSelectedPieceOfFurniture();
4799     if (selectedPiece != null
4800         && selectedPiece.isResizable()
4801         && isItemResizable(selectedPiece)) {
4802       return selectedPiece;
4803     }
4804     return null;
4805   }
4806 
4807   /**
4808    * Returns the selected item if selection contains one selected piece of furniture.
4809    */
4810   private HomePieceOfFurniture getSelectedPieceOfFurniture() {
4811     List<Selectable> selectedItems = this.home.getSelectedItems();
4812     if (selectedItems.size() == 1
4813         && selectedItems.get(0) instanceof HomePieceOfFurniture) {
4814       HomePieceOfFurniture piece = (HomePieceOfFurniture)selectedItems.get(0);
4815       if (isPieceOfFurnitureVisibleAtSelectedLevel(piece)) {
4816         return piece;
4817       }
4818     }
4819     return null;
4820   }
4821 
4822   /**
4823    * Returns the selected light with a point at (<code>x</code>, <code>y</code>)
4824    * that can be used to resize the power of the light.
4825    */
4826   private HomeLight getModifiedLightPowerAt(float x, float y) {
4827     HomePieceOfFurniture selectedPiece = getSelectedPieceOfFurniture();
4828     if (selectedPiece instanceof HomeLight) {
4829       float margin = getIndicatorMargin();
4830       if (selectedPiece.isBottomLeftPointAt(x, y, margin)
4831           // Ignore piece shape to ensure there's always enough space to drag it
4832           && !selectedPiece.containsPoint(x, y, 0)) {
4833         return (HomeLight)selectedPiece;
4834       }
4835     }
4836     return null;
4837   }
4838 
4839   /**
4840    * Returns the selected piece of furniture with its
4841    * name center point at (<code>x</code>, <code>y</code>).
4842    */
4843   private HomePieceOfFurniture getPieceOfFurnitureNameAt(float x, float y) {
4844     HomePieceOfFurniture selectedPiece = getSelectedMovablePieceOfFurniture();
4845     if (selectedPiece != null) {
4846       float margin = getIndicatorMargin();
4847       if (selectedPiece.isNameVisible()
4848           && selectedPiece.getName().trim().length() > 0
4849           && selectedPiece.isNameCenterPointAt(x, y, margin)) {
4850         return selectedPiece;
4851       }
4852     }
4853     return null;
4854   }
4855 
4856   /**
4857    * Returns the selected piece of furniture with its
4858    * name angle point at (<code>x</code>, <code>y</code>).
4859    */
4860   private HomePieceOfFurniture getPieceOfFurnitureRotatedNameAt(float x, float y) {
4861     HomePieceOfFurniture selectedPiece = getSelectedMovablePieceOfFurniture();
4862     if (selectedPiece != null) {
4863       float margin = getIndicatorMargin();
4864       if (selectedPiece.isNameVisible()
4865           && selectedPiece.getName().trim().length() > 0
4866           && isTextAnglePointAt(selectedPiece, selectedPiece.getName(), selectedPiece.getNameStyle(),
4867                 selectedPiece.getX() + selectedPiece.getNameXOffset(), selectedPiece.getY() + selectedPiece.getNameYOffset(),
4868                 selectedPiece.getNameAngle(), x, y, margin)) {
4869         return selectedPiece;
4870       }
4871     }
4872     return null;
4873   }
4874 
4875   /**
4876    * Returns <code>true</code> if the angle indicator of the <code>text</code> of an <code>item</code> displayed
4877    * at the point (<code>xText</code>, <code>yText</code>) is equal to the point (<code>x</code>, <code>y</code>).
4878    */
4879   private boolean isTextAnglePointAt(Selectable item, String text, TextStyle textStyle, float xText, float yText, float textAngle,
4880                                      float x, float y, float margin) {
4881     if (textStyle == null) {
4882       textStyle = this.preferences.getDefaultTextStyle(item.getClass());
4883     }
4884     float [][] textBounds = getView().getTextBounds(text, textStyle, xText, yText, textAngle);
4885     float anglePointX;
4886     float anglePointY;
4887     if (textStyle.getAlignment() == TextStyle.Alignment.LEFT) {
4888       anglePointX = textBounds [0][0];
4889       anglePointY = textBounds [0][1];
4890     } else if (textStyle.getAlignment() == TextStyle.Alignment.RIGHT) {
4891       anglePointX = textBounds [1][0];
4892       anglePointY = textBounds [1][1];
4893     } else { // CENTER
4894       anglePointX = (textBounds [0][0] + textBounds [1][0]) / 2;
4895       anglePointY = (textBounds [0][1] + textBounds [1][1]) / 2;
4896     }
4897     return Math.abs(x - anglePointX) <= margin
4898         && Math.abs(y - anglePointY) <= margin;
4899   }
4900 
4901   /**
4902    * Returns the selected label with its angle point at (<code>x</code>, <code>y</code>).
4903    */
4904   private Label getRotatedLabelAt(float x, float y) {
4905     List<Selectable> selectedItems = this.home.getSelectedItems();
4906     if (selectedItems.size() == 1
4907         && selectedItems.get(0) instanceof Label
4908         && isItemMovable(selectedItems.get(0))) {
4909       Label label = (Label)selectedItems.get(0);
4910       float margin = getIndicatorMargin();
4911       if (label.isAtLevel(this.home.getSelectedLevel())
4912           && isTextAnglePointAt(label, label.getText(), label.getStyle(),
4913                 label.getX(), label.getY(), label.getAngle(), x, y, margin)) {
4914         return label;
4915       }
4916     }
4917     return null;
4918   }
4919 
4920   /**
4921    * Returns the selected label with its elevation point at (<code>x</code>, <code>y</code>).
4922    */
4923   private Label getElevatedLabelAt(float x, float y) {
4924     List<Selectable> selectedItems = this.home.getSelectedItems();
4925     if (selectedItems.size() == 1
4926         && selectedItems.get(0) instanceof Label) {
4927       Label label = (Label)selectedItems.get(0);
4928       if (label.getPitch() != null
4929           && isItemMovable(label)) {
4930         float margin = getIndicatorMargin();
4931         if (label.isAtLevel(this.home.getSelectedLevel())) {
4932           TextStyle style = label.getStyle();
4933           if (style == null) {
4934             style = this.preferences.getDefaultTextStyle(label.getClass());
4935           }
4936           float [][] textBounds = getView().getTextBounds(label.getText(), getItemTextStyle(label, label.getStyle()),
4937               label.getX(), label.getY(), label.getAngle());
4938           float pointX;
4939           float pointY;
4940           if (style.getAlignment() == TextStyle.Alignment.LEFT) {
4941             pointX = textBounds [3][0];
4942             pointY = textBounds [3][1];
4943           } else if (style.getAlignment() == TextStyle.Alignment.RIGHT) {
4944             pointX = textBounds [2][0];
4945             pointY = textBounds [2][1];
4946           } else { // CENTER
4947             pointX = (textBounds [2][0] + textBounds [3][0]) / 2;
4948             pointY = (textBounds [2][1] + textBounds [3][1]) / 2;
4949           }
4950           if (Math.abs(x - pointX) <= margin
4951               && Math.abs(y - pointY) <= margin) {
4952             return label;
4953           }
4954         }
4955       }
4956     }
4957     return null;
4958   }
4959 
4960   /**
4961    * Returns the selected camera with a point at (<code>x</code>, <code>y</code>)
4962    * that can be used to change the camera yaw angle.
4963    */
4964   private Camera getYawRotatedCameraAt(float x, float y) {
4965     List<Selectable> selectedItems = this.home.getSelectedItems();
4966     if (selectedItems.size() == 1
4967         && selectedItems.get(0) instanceof Camera
4968         && isItemResizable(selectedItems.get(0))) {
4969       ObserverCamera camera = (ObserverCamera)selectedItems.get(0);
4970       float margin = getIndicatorMargin();
4971       float [][] cameraPoints = camera.getPoints();
4972       // Check if (x,y) matches the point between the first and the last points
4973       // of the rectangle surrounding camera
4974       float xMiddleFirstAndLastPoint = (cameraPoints [0][0] + cameraPoints [3][0]) / 2;
4975       float yMiddleFirstAndLastPoint = (cameraPoints [0][1] + cameraPoints [3][1]) / 2;
4976       if (Math.abs(x - xMiddleFirstAndLastPoint) <= margin
4977           && Math.abs(y - yMiddleFirstAndLastPoint) <= margin
4978           // Ignore camera shape to ensure there's always enough space to drag it
4979           && !camera.containsPoint(x, y, 0)) {
4980         return camera;
4981       }
4982     }
4983     return null;
4984   }
4985 
4986   /**
4987    * Returns the selected camera with a point at (<code>x</code>, <code>y</code>)
4988    * that can be used to change the camera pitch angle.
4989    */
4990   private Camera getPitchRotatedCameraAt(float x, float y) {
4991     List<Selectable> selectedItems = this.home.getSelectedItems();
4992     if (selectedItems.size() == 1
4993         && selectedItems.get(0) instanceof Camera
4994         && isItemResizable(selectedItems.get(0))) {
4995       ObserverCamera camera = (ObserverCamera)selectedItems.get(0);
4996       float margin = getIndicatorMargin();
4997       float [][] cameraPoints = camera.getPoints();
4998       // Check if (x,y) matches the point between the second and the third points
4999       // of the rectangle surrounding camera
5000       float xMiddleFirstAndLastPoint = (cameraPoints [1][0] + cameraPoints [2][0]) / 2;
5001       float yMiddleFirstAndLastPoint = (cameraPoints [1][1] + cameraPoints [2][1]) / 2;
5002       if (Math.abs(x - xMiddleFirstAndLastPoint) <= margin
5003           && Math.abs(y - yMiddleFirstAndLastPoint) <= margin
5004           // Ignore camera shape to ensure there's always enough space to drag it
5005           && !camera.containsPoint(x, y, 0)) {
5006         return camera;
5007       }
5008     }
5009     return null;
5010   }
5011 
5012   /**
5013    * Returns the selected camera with a point at (<code>x</code>, <code>y</code>)
5014    * that can be used to change the camera elevation.
5015    */
5016   private Camera getElevatedCameraAt(float x, float y) {
5017     List<Selectable> selectedItems = this.home.getSelectedItems();
5018     if (selectedItems.size() == 1
5019         && selectedItems.get(0) instanceof Camera
5020         && isItemResizable(selectedItems.get(0))) {
5021       ObserverCamera camera = (ObserverCamera)selectedItems.get(0);
5022       float margin = getIndicatorMargin();
5023       float [][] cameraPoints = camera.getPoints();
5024       // Check if (x,y) matches the point between the first and the second points
5025       // of the rectangle surrounding camera
5026       float xMiddleFirstAndSecondPoint = (cameraPoints [0][0] + cameraPoints [1][0]) / 2;
5027       float yMiddleFirstAndSecondPoint = (cameraPoints [0][1] + cameraPoints [1][1]) / 2;
5028       if (Math.abs(x - xMiddleFirstAndSecondPoint) <= margin
5029           && Math.abs(y - yMiddleFirstAndSecondPoint) <= margin
5030           // Ignore camera shape to ensure there's always enough space to drag it
5031           && !camera.containsPoint(x, y, 0)) {
5032         return camera;
5033       }
5034     }
5035     return null;
5036   }
5037 
5038   /**
5039    * Returns the selected compass with a point
5040    * at (<code>x</code>, <code>y</code>) that can be used to rotate it.
5041    */
5042   private Compass getRotatedCompassAt(float x, float y) {
5043     List<Selectable> selectedItems = this.home.getSelectedItems();
5044     if (selectedItems.size() == 1
5045         && selectedItems.get(0) instanceof Compass
5046         && isItemMovable(selectedItems.get(0))) {
5047       Compass compass = (Compass)selectedItems.get(0);
5048       float margin = getIndicatorMargin();
5049       float [][] compassPoints = compass.getPoints();
5050       // Check if (x,y) matches the point between the third and the fourth points (South point)
5051       // of the rectangle surrounding compass
5052       float xMiddleThirdAndFourthPoint = (compassPoints [2][0] + compassPoints [3][0]) / 2;
5053       float yMiddleThirdAndFourthPoint = (compassPoints [2][1] + compassPoints [3][1]) / 2;
5054       if (Math.abs(x - xMiddleThirdAndFourthPoint) <= margin
5055           && Math.abs(y - yMiddleThirdAndFourthPoint) <= margin
5056           // Ignore camera shape to ensure there's always enough space to drag it
5057           && !compass.containsPoint(x, y, 0)) {
5058         return compass;
5059       }
5060     }
5061     return null;
5062   }
5063 
5064   /**
5065    * Returns the selected compass with a point
5066    * at (<code>x</code>, <code>y</code>) that can be used to resize it.
5067    */
5068   private Compass getResizedCompassAt(float x, float y) {
5069     List<Selectable> selectedItems = this.home.getSelectedItems();
5070     if (selectedItems.size() == 1
5071         && selectedItems.get(0) instanceof Compass
5072         && isItemMovable(selectedItems.get(0))) {
5073       Compass compass = (Compass)selectedItems.get(0);
5074       float margin = getIndicatorMargin();
5075       float [][] compassPoints = compass.getPoints();
5076       // Check if (x,y) matches the point between the second and the third points (East point)
5077       // of the rectangle surrounding compass
5078       float xMiddleSecondAndThirdPoint = (compassPoints [1][0] + compassPoints [2][0]) / 2;
5079       float yMiddleSecondAndThirdPoint = (compassPoints [1][1] + compassPoints [2][1]) / 2;
5080       if (Math.abs(x - xMiddleSecondAndThirdPoint) <= margin
5081           && Math.abs(y - yMiddleSecondAndThirdPoint) <= margin
5082           // Ignore camera shape to ensure there's always enough space to drag it
5083           && !compass.containsPoint(x, y, 0)) {
5084         return compass;
5085       }
5086     }
5087     return null;
5088   }
5089 
5090   /**
5091    * Deletes <code>items</code> in plan and record it as an undoable operation.
5092    */
5093   public void deleteItems(List<? extends Selectable> items) {
5094     List<Selectable> deletedItems = new ArrayList<Selectable>(items.size());
5095     for (Selectable item : items) {
5096       if (isItemDeletable(item)) {
5097         deletedItems.add(item);
5098       }
5099     }
5100 
5101     if (!deletedItems.isEmpty()) {
5102       this.undoSupport.beginUpdate();
5103       // Remove selectionListener to avoid level selection change
5104       // when selected items are deleted
5105       this.home.removeSelectionListener(this.selectionListener);
5106       // Start a compound edit that deletes walls, furniture and dimension lines from home
5107 
5108       final boolean allLevelsSelection = home.isAllLevelsSelection();
5109       final List<Selectable> selectedItems = new ArrayList<Selectable>(items);
5110       // Add a undoable edit that will select the undeleted items at undo
5111       this.undoSupport.postEdit(new ItemsDeletionStartUndoableEdit(this, this.home,
5112           allLevelsSelection, selectedItems.toArray(new Selectable [selectedItems.size()])));
5113 
5114       // Delete furniture with inherited method
5115       deleteFurniture(Home.getFurnitureSubList(deletedItems));
5116 
5117       List<Selectable> deletedOtherItems =
5118           new ArrayList<Selectable>(Home.getWallsSubList(deletedItems));
5119       deletedOtherItems.addAll(Home.getRoomsSubList(deletedItems));
5120       deletedOtherItems.addAll(Home.getDimensionLinesSubList(deletedItems));
5121       deletedOtherItems.addAll(Home.getPolylinesSubList(deletedItems));
5122       deletedOtherItems.addAll(Home.getLabelsSubList(deletedItems));
5123       // First post to undo support that walls, rooms and dimension lines are deleted,
5124       // otherwise data about joined walls and rooms index can't be stored
5125       postDeleteItems(deletedOtherItems, this.home.isBasePlanLocked(), this.home.isAllLevelsSelection());
5126       // Then delete items from plan
5127       doDeleteItems(deletedOtherItems);
5128       this.home.addSelectionListener(this.selectionListener);
5129 
5130       this.undoSupport.postEdit(new ItemsDeletionEndUndoableEdit(this, this.home));
5131 
5132       // End compound edit
5133       this.undoSupport.endUpdate();
5134     }
5135   }
5136 
5137   /**
5138    * Undoable edit for items deletion start.
5139    */
5140   private static class ItemsDeletionStartUndoableEdit extends AbstractUndoableEdit {
5141     private final PlanController controller;
5142     private final Home           home;
5143     private final boolean        allLevelsSelection;
5144     private final Selectable []  selectedItems;
5145 
5146     public ItemsDeletionStartUndoableEdit(PlanController controller, Home home,
5147                                           boolean allLevelsSelection, Selectable [] selectedItems) {
5148       this.controller = controller;
5149       this.home = home;
5150       this.allLevelsSelection = allLevelsSelection;
5151       this.selectedItems = selectedItems;
5152     }
5153 
5154     @Override
5155     public void undo() throws CannotRedoException {
5156       super.undo();
5157       this.controller.selectAndShowItems(Arrays.asList(this.selectedItems), this.allLevelsSelection);
5158     }
5159 
5160     @Override
5161     public void redo() throws CannotRedoException {
5162       super.redo();
5163       this.home.removeSelectionListener(this.controller.getSelectionListener());
5164     }
5165   }
5166 
5167   /**
5168    * Undoable edit for items deletion end.
5169    */
5170   private static class ItemsDeletionEndUndoableEdit extends AbstractUndoableEdit {
5171     private PlanController controller;
5172     private Home           home;
5173 
5174     public ItemsDeletionEndUndoableEdit(PlanController controller, Home home) {
5175       this.controller = controller;
5176       this.home = home;
5177 
5178     }
5179 
5180     @Override
5181     public void redo() throws CannotRedoException {
5182       super.redo();
5183       this.home.addSelectionListener(this.controller.getSelectionListener());
5184     }
5185   }
5186 
5187   /**
5188    * Posts an undoable delete items operation about <code>deletedItems</code>.
5189    */
5190   private void postDeleteItems(final List<? extends Selectable> deletedItems,
5191                                final boolean basePlanLocked,
5192                                final boolean allLevelsSelection) {
5193     // Manage walls
5194     List<Wall> deletedWalls = Home.getWallsSubList(deletedItems);
5195     // Get joined walls data for undo operation
5196     final JoinedWall [] joinedDeletedWalls = JoinedWall.getJoinedWalls(deletedWalls);
5197 
5198     // Manage rooms and their index
5199     List<Room> deletedRooms = Home.getRoomsSubList(deletedItems);
5200     List<Room> homeRooms = this.home.getRooms();
5201     // Sort the deleted rooms in the ascending order of their index in home
5202     TreeMap<Integer, Room> sortedMap = new TreeMap<Integer, Room>();
5203     for (Room room : deletedRooms) {
5204       sortedMap.put(homeRooms.indexOf(room), room);
5205     }
5206     final Room [] rooms = sortedMap.values().toArray(new Room [sortedMap.size()]);
5207     final int [] roomsIndices = new int [rooms.length];
5208     final Level [] roomsLevels = new Level [rooms.length];
5209     int i = 0;
5210     for (int index : sortedMap.keySet()) {
5211       roomsIndices [i] = index;
5212       roomsLevels [i] = rooms [i].getLevel();
5213       i++;
5214     }
5215 
5216     // Manage dimension lines
5217     List<DimensionLine> deletedDimensionLines = Home.getDimensionLinesSubList(deletedItems);
5218     final DimensionLine [] dimensionLines = deletedDimensionLines.toArray(
5219         new DimensionLine [deletedDimensionLines.size()]);
5220     final Level [] dimensionLinesLevels = new Level [dimensionLines.length];
5221     for (i = 0; i < dimensionLines.length; i++) {
5222       dimensionLinesLevels [i] = dimensionLines [i].getLevel();
5223     }
5224 
5225     // Manage polylines and their index
5226     List<Polyline> deletedPolylines = Home.getPolylinesSubList(deletedItems);
5227     List<Polyline> homePolylines = this.home.getPolylines();
5228     // Sort the deleted polylines in the ascending order of their index in home
5229     TreeMap<Integer, Polyline> sortedPolylinesMap = new TreeMap<Integer, Polyline>();
5230     for (Polyline polyline : deletedPolylines) {
5231       sortedPolylinesMap.put(homePolylines.indexOf(polyline), polyline);
5232     }
5233     final Polyline [] polylines = sortedPolylinesMap.values().toArray(new Polyline [sortedPolylinesMap.size()]);
5234     final int [] polylinesIndices = new int [polylines.length];
5235     final Level [] polylinesLevels = new Level [polylines.length];
5236     i = 0;
5237     for (int index : sortedPolylinesMap.keySet()) {
5238       polylinesIndices [i] = index;
5239       polylinesLevels [i] = polylines [i].getLevel();
5240       i++;
5241     }
5242 
5243     // Manage labels
5244     List<Label> deletedLabels = Home.getLabelsSubList(deletedItems);
5245     final Label [] labels = deletedLabels.toArray(new Label [deletedLabels.size()]);
5246     final Level [] labelsLevels = new Level [labels.length];
5247     for (i = 0; i < labels.length; i++) {
5248       labelsLevels [i] = labels [i].getLevel();
5249     }
5250 
5251     this.undoSupport.postEdit(new ItemsDeletionUndoableEdit(this, this.preferences, basePlanLocked, allLevelsSelection,
5252         deletedItems.toArray(new Selectable [deletedItems.size()]), joinedDeletedWalls,
5253         rooms, roomsIndices, roomsLevels,
5254         dimensionLines, dimensionLinesLevels,
5255         polylines, polylinesIndices, polylinesLevels,
5256         labels, labelsLevels));
5257   }
5258 
5259   /**
5260    * Undoable edit for items deletion.
5261    */
5262   private static class ItemsDeletionUndoableEdit extends LocalizedUndoableEdit {
5263     private final PlanController   controller;
5264     private final boolean          basePlanLocked;
5265     private final boolean          allLevelsSelection;
5266     private final Selectable []    deletedItems;
5267     private final JoinedWall []    joinedDeletedWalls;
5268     private final Room []          rooms;
5269     private final int []           roomsIndices;
5270     private final Level []         roomsLevels;
5271     private final DimensionLine [] dimensionLines;
5272     private final Level []         dimensionLinesLevels;
5273     private final Polyline []      polylines;
5274     private final int []           polylinesIndices;
5275     private final Level []         polylinesLevels;
5276     private final Label []         labels;
5277     private final Level []         labelsLevels;
5278 
5279     public ItemsDeletionUndoableEdit(PlanController controller, UserPreferences preferences,
5280                                      boolean basePlanLocked, boolean allLevelsSelection,
5281                                      Selectable []  deletedItems,
5282                                      JoinedWall [] joinedDeletedWalls,
5283                                      Room [] rooms, int [] roomsIndices, Level [] roomsLevels,
5284                                      DimensionLine [] dimensionLines, Level [] dimensionLinesLevels,
5285                                      Polyline [] polylines, int [] polylinesIndices, Level [] polylinesLevels,
5286                                      Label [] labels, Level [] labelsLevels) {
5287       super(preferences, PlanController.class, "undoDeleteSelectionName");
5288       this.controller = controller;
5289       this.basePlanLocked = basePlanLocked;
5290       this.allLevelsSelection = allLevelsSelection;
5291       this.deletedItems = deletedItems;
5292       this.joinedDeletedWalls = joinedDeletedWalls;
5293       this.rooms = rooms;
5294       this.roomsIndices = roomsIndices;
5295       this.roomsLevels = roomsLevels;
5296       this.dimensionLines = dimensionLines;
5297       this.dimensionLinesLevels = dimensionLinesLevels;
5298       this.polylines = polylines;
5299       this.polylinesIndices = polylinesIndices;
5300       this.polylinesLevels = polylinesLevels;
5301       this.labels = labels;
5302       this.labelsLevels = labelsLevels;
5303     }
5304 
5305     @Override
5306     public void undo() throws CannotUndoException {
5307       super.undo();
5308       this.controller.doAddWalls(this.joinedDeletedWalls, this.basePlanLocked);
5309       this.controller.doAddRooms(this.rooms, this.roomsIndices, this.roomsLevels, null, this.basePlanLocked);
5310       this.controller.doAddDimensionLines(this.dimensionLines, this.dimensionLinesLevels, null, this.basePlanLocked);
5311       this.controller.doAddPolylines(this.polylines, this.polylinesIndices, this.polylinesLevels, null, this.basePlanLocked);
5312       this.controller.doAddLabels(this.labels, this.labelsLevels, null, this.basePlanLocked);
5313       this.controller.selectAndShowItems(Arrays.asList(this.deletedItems), this.allLevelsSelection);
5314     }
5315 
5316     @Override
5317     public void redo() throws CannotRedoException {
5318       super.redo();
5319       this.controller.selectItems(Arrays.asList(this.deletedItems));
5320       this.controller.doDeleteWalls(this.joinedDeletedWalls, this.basePlanLocked);
5321       this.controller.doDeleteRooms(this.rooms, this.basePlanLocked);
5322       this.controller.doDeleteDimensionLines(this.dimensionLines, this.basePlanLocked);
5323       this.controller.doDeletePolylines(this.polylines, this.basePlanLocked);
5324       this.controller.doDeleteLabels(this.labels, this.basePlanLocked);
5325     }
5326   }
5327 
5328   /**
5329    * Deletes <code>items</code> from home.
5330    */
5331   private void doDeleteItems(List<Selectable> items) {
5332     boolean basePlanLocked = this.home.isBasePlanLocked();
5333     for (Selectable item : items) {
5334       if (item instanceof Wall) {
5335         home.deleteWall((Wall)item);
5336       } else if (item instanceof DimensionLine) {
5337         home.deleteDimensionLine((DimensionLine)item);
5338       } else if (item instanceof Room) {
5339         home.deleteRoom((Room)item);
5340       } else if (item instanceof Polyline) {
5341         home.deletePolyline((Polyline)item);
5342       } else if (item instanceof Label) {
5343         home.deleteLabel((Label)item);
5344       } else if (item instanceof HomePieceOfFurniture) {
5345         home.deletePieceOfFurniture((HomePieceOfFurniture)item);
5346       }
5347       // Unlock base plan if item is a part of it
5348       basePlanLocked &= !isItemPartOfBasePlan(item);
5349     }
5350     this.home.setBasePlanLocked(basePlanLocked);
5351     this.home.setAllLevelsSelection(false);
5352   }
5353 
5354   /**
5355    * Moves and shows selected items in plan component of (<code>dx</code>,
5356    * <code>dy</code>) units and record it as undoable operation.
5357    */
5358   private void moveAndShowSelectedItems(float dx, float dy) {
5359     List<Selectable> selectedItems = this.home.getSelectedItems();
5360     List<Selectable> movedItems = new ArrayList<Selectable>(selectedItems.size());
5361     for (Selectable item : selectedItems) {
5362       if (isItemMovable(item)) {
5363         movedItems.add(item);
5364       }
5365     }
5366 
5367     if (!movedItems.isEmpty()) {
5368       moveItems(movedItems, dx, dy);
5369       selectAndShowItems(movedItems, this.home.isAllLevelsSelection());
5370       if (movedItems.size() != 1
5371           || !(movedItems.get(0) instanceof Camera)) {
5372         // Post move undo only for items different from the camera
5373         postItemsMove(movedItems, selectedItems, dx, dy);
5374       }
5375     }
5376   }
5377 
5378   /**
5379    * Moves <code>items</code> of (<code>dx</code>, <code>dy</code>) units.
5380    */
5381   public void moveItems(List<? extends Selectable> items, float dx, float dy) {
5382     for (Selectable item : items) {
5383       if (item instanceof Wall) {
5384         Wall wall = (Wall)item;
5385         // Remove temporarily listener to avoid side effect
5386         wall.removePropertyChangeListener(this.wallChangeListener);
5387         moveWallStartPoint(wall,
5388             wall.getXStart() + dx, wall.getYStart() + dy,
5389             !items.contains(wall.getWallAtStart()));
5390         moveWallEndPoint(wall,
5391             wall.getXEnd() + dx, wall.getYEnd() + dy,
5392             !items.contains(wall.getWallAtEnd()));
5393         resetAreaCache();
5394         wall.addPropertyChangeListener(this.wallChangeListener);
5395       } else {
5396         boolean boundToWall = false;
5397         if (item instanceof HomeDoorOrWindow) {
5398           boundToWall = ((HomeDoorOrWindow)item).isBoundToWall();
5399         }
5400         item.move(dx, dy);
5401         if (boundToWall) {
5402           Area itemArea = new Area(getPath(item.getPoints()));
5403           itemArea.intersect(getWallsArea(true));
5404           ((HomeDoorOrWindow)item).setBoundToWall(!itemArea.isEmpty());
5405         }
5406       }
5407     }
5408   }
5409 
5410   /**
5411    * Moves <code>wall</code> start point to (<code>xStart</code>, <code>yStart</code>)
5412    * and the wall point joined to its start point if <code>moveWallAtStart</code> is true.
5413    */
5414   private static void moveWallStartPoint(Wall wall, float xStart, float yStart,
5415                                          boolean moveWallAtStart) {
5416     float oldXStart = wall.getXStart();
5417     float oldYStart = wall.getYStart();
5418     wall.setXStart(xStart);
5419     wall.setYStart(yStart);
5420     Wall wallAtStart = wall.getWallAtStart();
5421     // If wall is joined to a wall at its start
5422     // and this wall doesn't belong to the list of moved walls
5423     if (wallAtStart != null && moveWallAtStart) {
5424       // Move the wall start point or end point
5425       if (wallAtStart.getWallAtStart() == wall
5426           && (wallAtStart.getWallAtEnd() != wall
5427               || (wallAtStart.getXStart() == oldXStart
5428                   && wallAtStart.getYStart() == oldYStart))) {
5429         wallAtStart.setXStart(xStart);
5430         wallAtStart.setYStart(yStart);
5431       } else if (wallAtStart.getWallAtEnd() == wall
5432                  && (wallAtStart.getWallAtStart() != wall
5433                      || (wallAtStart.getXEnd() == oldXStart
5434                          && wallAtStart.getYEnd() == oldYStart))) {
5435         wallAtStart.setXEnd(xStart);
5436         wallAtStart.setYEnd(yStart);
5437       }
5438     }
5439   }
5440 
5441   /**
5442    * Moves <code>wall</code> end point to (<code>xEnd</code>, <code>yEnd</code>)
5443    * and the wall point joined to its end if <code>moveWallAtEnd</code> is true.
5444    */
5445   private static void moveWallEndPoint(Wall wall, float xEnd, float yEnd,
5446                                        boolean moveWallAtEnd) {
5447     float oldXEnd = wall.getXEnd();
5448     float oldYEnd = wall.getYEnd();
5449     wall.setXEnd(xEnd);
5450     wall.setYEnd(yEnd);
5451     Wall wallAtEnd = wall.getWallAtEnd();
5452     // If wall is joined to a wall at its end
5453     // and this wall doesn't belong to the list of moved walls
5454     if (wallAtEnd != null && moveWallAtEnd) {
5455       // Move the wall start point or end point
5456       if (wallAtEnd.getWallAtStart() == wall
5457           && (wallAtEnd.getWallAtEnd() != wall
5458               || (wallAtEnd.getXStart() == oldXEnd
5459                   && wallAtEnd.getYStart() == oldYEnd))) {
5460         wallAtEnd.setXStart(xEnd);
5461         wallAtEnd.setYStart(yEnd);
5462       } else if (wallAtEnd.getWallAtEnd() == wall
5463                  && (wallAtEnd.getWallAtStart() != wall
5464                      || (wallAtEnd.getXEnd() == oldXEnd
5465                          && wallAtEnd.getYEnd() == oldYEnd))) {
5466         wallAtEnd.setXEnd(xEnd);
5467         wallAtEnd.setYEnd(yEnd);
5468       }
5469     }
5470   }
5471 
5472   /**
5473    * Moves <code>wall</code> start point to (<code>x</code>, <code>y</code>)
5474    * if <code>editingStartPoint</code> is true or <code>wall</code> end point
5475    * to (<code>x</code>, <code>y</code>) if <code>editingStartPoint</code> is false.
5476    */
5477   private static void moveWallPoint(Wall wall, float x, float y, boolean startPoint) {
5478     if (startPoint) {
5479       moveWallStartPoint(wall, x, y, true);
5480     } else {
5481       moveWallEndPoint(wall, x, y, true);
5482     }
5483   }
5484 
5485   /**
5486    * Moves <code>room</code> point at the given index to (<code>x</code>, <code>y</code>).
5487    */
5488   private static void moveRoomPoint(Room room, float x, float y, int pointIndex) {
5489     room.setPoint(x, y, pointIndex);
5490   }
5491 
5492   /**
5493    * Moves <code>dimensionLine</code> start point to (<code>x</code>, <code>y</code>)
5494    * if <code>editingStartPoint</code> is true or <code>dimensionLine</code> end point
5495    * to (<code>x</code>, <code>y</code>) if <code>editingStartPoint</code> is false.
5496    */
5497   private static void moveDimensionLinePoint(DimensionLine dimensionLine, float x, float y, boolean startPoint) {
5498     if (startPoint) {
5499       dimensionLine.setXStart(x);
5500       dimensionLine.setYStart(y);
5501     } else {
5502       dimensionLine.setXEnd(x);
5503       dimensionLine.setYEnd(y);
5504     }
5505   }
5506 
5507   /**
5508    * Swaps start and end points of the given dimension line.
5509    */
5510   private static void reverseDimensionLine(DimensionLine dimensionLine) {
5511     float swappedX = dimensionLine.getXStart();
5512     float swappedY = dimensionLine.getYStart();
5513     dimensionLine.setXStart(dimensionLine.getXEnd());
5514     dimensionLine.setYStart(dimensionLine.getYEnd());
5515     dimensionLine.setXEnd(swappedX);
5516     dimensionLine.setYEnd(swappedY);
5517     dimensionLine.setOffset(-dimensionLine.getOffset());
5518   }
5519 
5520   /**
5521    * Selects <code>items</code> and make them visible at screen.
5522    */
5523   protected void selectAndShowItems(List<? extends Selectable> items) {
5524     selectAndShowItems(items, false);
5525   }
5526 
5527   /**
5528    * Selects <code>items</code> and make them visible at screen.
5529    */
5530   private void selectAndShowItems(List<? extends Selectable> items, boolean allLevelsSelection) {
5531     selectItems(items, allLevelsSelection);
5532     selectLevelFromSelectedItems();
5533     getView().makeSelectionVisible();
5534   }
5535 
5536   /**
5537    * Selects <code>items</code>.
5538    */
5539   protected void selectItems(List<? extends Selectable> items) {
5540     this.selectItems(items, false);
5541   }
5542 
5543   /**
5544    * Selects <code>items</code>.
5545    */
5546   private void selectItems(List<? extends Selectable> items, boolean allLevelsSelection) {
5547     // Remove selectionListener when selection is done from this controller
5548     // to control when selection should be made visible
5549     this.home.removeSelectionListener(this.selectionListener);
5550     this.home.setSelectedItems(items);
5551     this.home.addSelectionListener(this.selectionListener);
5552     this.home.setAllLevelsSelection(allLevelsSelection);
5553   }
5554 
5555   /**
5556    * Selects the given <code>item</code>.
5557    */
5558   public void selectItem(Selectable item) {
5559     selectItems(Arrays.asList(new Selectable [] {item}));
5560   }
5561 
5562   /**
5563    * Toggles the selection of the given <code>item</code>.
5564    * @since 4.4
5565    */
5566   public void toggleItemSelection(Selectable item) {
5567     List<Selectable> selectedItems = new ArrayList<Selectable>(this.home.getSelectedItems());
5568     if (selectedItems.contains(item)) {
5569       selectedItems.remove(item);
5570     } else {
5571       selectedItems.add(item);
5572     }
5573     selectItems(selectedItems, this.home.isAllLevelsSelection());
5574   }
5575 
5576   /**
5577    * Deselects all walls in plan.
5578    */
5579   private void deselectAll() {
5580     List<Selectable> emptyList = Collections.emptyList();
5581     selectItems(emptyList);
5582   }
5583 
5584   /**
5585    * Adds <code>items</code> to home and post an undoable operation.
5586    */
5587   public void addItems(final List<? extends Selectable> items) {
5588     // Start a compound edit that adds walls, furniture, rooms, dimension lines and labels to home
5589     this.undoSupport.beginUpdate();
5590     addFurniture(Home.getFurnitureSubList(items));
5591     addWalls(Home.getWallsSubList(items));
5592     addRooms(Home.getRoomsSubList(items));
5593     addPolylines(Home.getPolylinesSubList(items));
5594     addDimensionLines(Home.getDimensionLinesSubList(items));
5595     addLabels(Home.getLabelsSubList(items));
5596     this.home.setSelectedItems(items);
5597 
5598     // Add a undoable edit that will select all the items at redo
5599     undoSupport.postEdit(new ItemsAdditionEndUndoableEdit(this.home, this.preferences,
5600        items.toArray(new Selectable [items.size()])));
5601     // End compound edit
5602     undoSupport.endUpdate();
5603   }
5604 
5605   /**
5606    * Undoable edit for items addition end.
5607    */
5608   private static class ItemsAdditionEndUndoableEdit extends LocalizedUndoableEdit {
5609     private final Home         home;
5610     private final Selectable[] items;
5611 
5612     private ItemsAdditionEndUndoableEdit(Home home, UserPreferences preferences, Selectable[] items) {
5613       super(preferences, PlanController.class, "undoAddItemsName");
5614       this.home = home;
5615       this.items = items;
5616     }
5617 
5618     @Override
5619     public void redo() throws CannotRedoException {
5620       super.redo();
5621       this.home.setSelectedItems(Arrays.asList(this.items));
5622     }
5623   }
5624 
5625   /**
5626    * Adds furniture to home and updates door and window flags if they intersect with walls and magnetism is enabled.
5627    */
5628   @Override
5629   public void addFurniture(List<HomePieceOfFurniture> furniture) {
5630     super.addFurniture(furniture);
5631     if (this.preferences.isMagnetismEnabled()) {
5632       Area wallsArea = getWallsArea(false);
5633       for (HomePieceOfFurniture piece : furniture) {
5634         if (piece instanceof HomeDoorOrWindow) {
5635           float [][] piecePoints = piece.getPoints();
5636           Area pieceAreaIntersection = new Area(getPath(piecePoints));
5637           pieceAreaIntersection.intersect(wallsArea);
5638           if (!pieceAreaIntersection.isEmpty()
5639               && new Room(piecePoints).getArea() / getArea(pieceAreaIntersection) > 0.999) {
5640             ((HomeDoorOrWindow) piece).setBoundToWall(true);
5641           }
5642         }
5643       }
5644     }
5645   }
5646 
5647   /**
5648    * Adds <code>walls</code> to home and post an undoable new wall operation.
5649    */
5650   public void addWalls(List<Wall> walls) {
5651     for (Wall wall : walls) {
5652       this.home.addWall(wall);
5653     }
5654     postCreateWalls(walls, this.home.getSelectedItems(),
5655         home.isBasePlanLocked(), home.isAllLevelsSelection());
5656   }
5657 
5658   /**
5659    * Posts an undoable new wall operation, about <code>newWalls</code>.
5660    */
5661   private void postCreateWalls(List<Wall> newWalls,
5662                                List<Selectable> oldSelectedItems,
5663                                final boolean oldBasePlanLocked,
5664                                final boolean oldAllLevelsSelection) {
5665     if (newWalls.size() > 0) {
5666       boolean basePlanLocked = this.home.isBasePlanLocked();
5667       if (basePlanLocked) {
5668         for (Wall wall : newWalls) {
5669           // Unlock base plan if wall is a part of it
5670           basePlanLocked &= !isItemPartOfBasePlan(wall);
5671         }
5672         this.home.setBasePlanLocked(basePlanLocked);
5673       }
5674       final boolean newBasePlanLocked = basePlanLocked;
5675 
5676       // Retrieve data about joined walls to newWalls
5677       final JoinedWall [] joinedNewWalls = JoinedWall.getJoinedWalls(newWalls);
5678       final Selectable [] oldSelection =
5679           oldSelectedItems.toArray(new Selectable [oldSelectedItems.size()]);
5680       this.undoSupport.postEdit(new WallsCreationUndoableEdit(this, this.preferences,
5681           oldSelection, oldBasePlanLocked, oldAllLevelsSelection,
5682           joinedNewWalls, newBasePlanLocked));
5683     }
5684   }
5685 
5686   /**
5687    * Undoable edit for walls creation.
5688    */
5689   private static class WallsCreationUndoableEdit extends LocalizedUndoableEdit {
5690     private final PlanController controller;
5691     private final Selectable []  oldSelection;
5692     private final boolean        oldBasePlanLocked;
5693     private final boolean        oldAllLevelsSelection;
5694     private final JoinedWall []  joinedNewWalls;
5695     private final boolean        newBasePlanLocked;
5696 
5697     public WallsCreationUndoableEdit(PlanController controller, UserPreferences preferences,
5698                                      Selectable [] oldSelection, boolean oldBasePlanLocked,
5699                                      boolean oldAllLevelsSelection, JoinedWall [] joinedNewWalls,
5700                                      boolean newBasePlanLocked) {
5701       super(preferences, PlanController.class, "undoCreateWallsName");
5702       this.controller = controller;
5703       this.oldSelection = oldSelection;
5704       this.oldBasePlanLocked = oldBasePlanLocked;
5705       this.oldAllLevelsSelection = oldAllLevelsSelection;
5706       this.joinedNewWalls = joinedNewWalls;
5707       this.newBasePlanLocked = newBasePlanLocked;
5708     }
5709 
5710     @Override
5711     public void undo() throws CannotUndoException {
5712       super.undo();
5713       this.controller.doDeleteWalls(this.joinedNewWalls, this.oldBasePlanLocked);
5714       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.oldAllLevelsSelection);
5715     }
5716 
5717     @Override
5718     public void redo() throws CannotRedoException {
5719       super.redo();
5720       this.controller.doAddWalls(this.joinedNewWalls, this.newBasePlanLocked);
5721       this.controller.selectAndShowItems(JoinedWall.getWalls(this.joinedNewWalls), false);
5722     }
5723   }
5724 
5725   /**
5726    * Adds the walls in <code>joinedWalls</code> to plan component, joins
5727    * them to other walls if necessary.
5728    */
5729   private void doAddWalls(JoinedWall [] joinedWalls, boolean basePlanLocked) {
5730     // First add all walls to home
5731     for (JoinedWall joinedNewWall : joinedWalls) {
5732       Wall wall = joinedNewWall.getWall();
5733       this.home.addWall(wall);
5734       wall.setLevel(joinedNewWall.getLevel());
5735     }
5736     this.home.setBasePlanLocked(basePlanLocked);
5737 
5738     // Then join them to each other if necessary
5739     for (JoinedWall joinedNewWall : joinedWalls) {
5740       Wall wall = joinedNewWall.getWall();
5741       Wall wallAtStart = joinedNewWall.getWallAtStart();
5742       if (wallAtStart != null) {
5743         wall.setWallAtStart(wallAtStart);
5744         if (joinedNewWall.isJoinedAtEndOfWallAtStart()) {
5745           wallAtStart.setWallAtEnd(wall);
5746         } else {
5747           wallAtStart.setWallAtStart(wall);
5748         }
5749       }
5750       Wall wallAtEnd = joinedNewWall.getWallAtEnd();
5751       if (wallAtEnd != null) {
5752         wall.setWallAtEnd(wallAtEnd);
5753         if (joinedNewWall.isJoinedAtStartOfWallAtEnd()) {
5754           wallAtEnd.setWallAtStart(wall);
5755         } else {
5756           wallAtEnd.setWallAtEnd(wall);
5757         }
5758       }
5759     }
5760   }
5761 
5762   /**
5763    * Deletes walls referenced in <code>joinedDeletedWalls</code>.
5764    */
5765   private void doDeleteWalls(JoinedWall [] joinedDeletedWalls,
5766                              boolean basePlanLocked) {
5767     for (JoinedWall joinedWall : joinedDeletedWalls) {
5768       this.home.deleteWall(joinedWall.getWall());
5769     }
5770     this.home.setBasePlanLocked(basePlanLocked);
5771   }
5772 
5773   /**
5774    * Add <code>newRooms</code> to home and post an undoable new room line operation.
5775    */
5776   public void addRooms(List<Room> rooms) {
5777     final Room [] newRooms = rooms.toArray(new Room [rooms.size()]);
5778     // Get indices of rooms added to home
5779     final int [] roomsIndex = new int [rooms.size()];
5780     int endIndex = home.getRooms().size();
5781     for (int i = 0; i < roomsIndex.length; i++) {
5782       roomsIndex [i] = endIndex++;
5783       this.home.addRoom(newRooms [i], roomsIndex [i]);
5784     }
5785     postCreateRooms(newRooms, roomsIndex, this.home.getSelectedItems(),
5786         this.home.isBasePlanLocked(), this.home.isAllLevelsSelection());
5787   }
5788 
5789   /**
5790    * Posts an undoable new room operation, about <code>newRooms</code>.
5791    */
5792   private void postCreateRooms(final Room [] newRooms,
5793                                final int [] roomsIndex,
5794                                List<Selectable> oldSelectedItems,
5795                                final boolean oldBasePlanLocked,
5796                                final boolean oldAllLevelsSelection) {
5797     if (newRooms.length > 0) {
5798       boolean basePlanLocked = this.home.isBasePlanLocked();
5799       if (basePlanLocked) {
5800         for (Room room : newRooms) {
5801           // Unlock base plan if room is a part of it
5802           basePlanLocked &= !isItemPartOfBasePlan(room);
5803         }
5804         this.home.setBasePlanLocked(basePlanLocked);
5805       }
5806       final boolean newBasePlanLocked = basePlanLocked;
5807 
5808       final Selectable [] oldSelection =
5809           oldSelectedItems.toArray(new Selectable [oldSelectedItems.size()]);
5810       final Level roomsLevel = this.home.getSelectedLevel();
5811       this.undoSupport.postEdit(new RoomsCreationUndoableEdit(this, this.preferences,
5812           oldSelection, oldBasePlanLocked, oldAllLevelsSelection,
5813           newRooms, roomsIndex, roomsLevel, newBasePlanLocked));
5814     }
5815   }
5816 
5817   /**
5818    * Undoable edit for rooms creation.
5819    */
5820   private static class RoomsCreationUndoableEdit extends LocalizedUndoableEdit {
5821     private final PlanController controller;
5822     private final Selectable []  oldSelection;
5823     private final boolean        oldBasePlanLocked;
5824     private final boolean        oldAllLevelsSelection;
5825     private final Room []        newRooms;
5826     private final int []         roomsIndex;
5827     private final Level          roomsLevel;
5828     private final boolean        newBasePlanLocked;
5829 
5830     public RoomsCreationUndoableEdit(PlanController controller, UserPreferences preferences,
5831                                      Selectable [] oldSelection, boolean oldBasePlanLocked, boolean oldAllLevelsSelection,
5832                                      Room [] newRooms, int [] roomsIndex, Level roomsLevel,
5833                                      boolean newBasePlanLocked) {
5834       super(preferences, PlanController.class, "undoCreateRoomsName");
5835       this.controller = controller;
5836       this.oldSelection = oldSelection;
5837       this.oldBasePlanLocked = oldBasePlanLocked;
5838       this.oldAllLevelsSelection = oldAllLevelsSelection;
5839       this.newRooms = newRooms;
5840       this.roomsIndex = roomsIndex;
5841       this.roomsLevel = roomsLevel;
5842       this.newBasePlanLocked = newBasePlanLocked;
5843     }
5844 
5845     @Override
5846     public void undo() throws CannotUndoException {
5847       super.undo();
5848       this.controller.doDeleteRooms(this.newRooms, this.oldBasePlanLocked);
5849       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.oldAllLevelsSelection);
5850     }
5851 
5852     @Override
5853     public void redo() throws CannotRedoException {
5854       super.redo();
5855       this.controller.doAddRooms(this.newRooms, this.roomsIndex, null, this.roomsLevel, this.newBasePlanLocked);
5856       this.controller.selectAndShowItems(Arrays.asList(this.newRooms), false);
5857     }
5858   }
5859 
5860   /**
5861    * Posts an undoable new room operation, about <code>newRooms</code>.
5862    */
5863   private void postCreateRooms(List<Room> rooms,
5864                                List<Selectable> oldSelection,
5865                                boolean basePlanLocked,
5866                                boolean allLevelsSelection) {
5867     // Search the index of rooms in home list of rooms
5868     Room [] newRooms = rooms.toArray(new Room [rooms.size()]);
5869     int [] roomsIndex = new int [rooms.size()];
5870     List<Room> homeRooms = this.home.getRooms();
5871     for (int i = 0; i < roomsIndex.length; i++) {
5872       roomsIndex [i] = homeRooms.lastIndexOf(newRooms [i]);
5873     }
5874     postCreateRooms(newRooms, roomsIndex, oldSelection, basePlanLocked, allLevelsSelection);
5875   }
5876 
5877   /**
5878    * Adds the <code>rooms</code> to plan component.
5879    */
5880   private void doAddRooms(Room [] rooms,
5881                           int [] roomsIndices,
5882                           Level [] roomsLevels,
5883                           Level uniqueRoomsLevel,
5884                           boolean basePlanLocked) {
5885     for (int i = 0; i < roomsIndices.length; i++) {
5886       this.home.addRoom (rooms [i], roomsIndices [i]);
5887       rooms [i].setLevel(roomsLevels != null
5888           ? roomsLevels [i]
5889           : uniqueRoomsLevel);
5890     }
5891     this.home.setBasePlanLocked(basePlanLocked);
5892   }
5893 
5894   /**
5895    * Deletes <code>rooms</code>.
5896    */
5897   private void doDeleteRooms(Room [] rooms,
5898                              boolean basePlanLocked) {
5899     for (Room room : rooms) {
5900       this.home.deleteRoom(room);
5901     }
5902     this.home.setBasePlanLocked(basePlanLocked);
5903   }
5904 
5905   /**
5906    * Add <code>dimensionLines</code> to home and post an undoable new dimension line operation.
5907    */
5908   public void addDimensionLines(List<DimensionLine> dimensionLines) {
5909     for (DimensionLine dimensionLine : dimensionLines) {
5910       this.home.addDimensionLine(dimensionLine);
5911     }
5912     postCreateDimensionLines(dimensionLines, this.home.getSelectedItems(),
5913         this.home.isBasePlanLocked(), this.home.isAllLevelsSelection());
5914   }
5915 
5916   /**
5917    * Posts an undoable new dimension line operation, about <code>newDimensionLines</code>.
5918    */
5919   private void postCreateDimensionLines(List<DimensionLine> newDimensionLines,
5920                                         List<Selectable> oldSelectedItems,
5921                                         final boolean oldBasePlanLocked,
5922                                         final boolean oldAllLevelsSelection) {
5923     if (newDimensionLines.size() > 0) {
5924       boolean basePlanLocked = this.home.isBasePlanLocked();
5925       if (basePlanLocked) {
5926         for (DimensionLine dimensionLine : newDimensionLines) {
5927           // Unlock base plan if dimension line is a part of it
5928           basePlanLocked &= !isItemPartOfBasePlan(dimensionLine);
5929         }
5930         this.home.setBasePlanLocked(basePlanLocked);
5931       }
5932       final boolean newBasePlanLocked = basePlanLocked;
5933 
5934       final DimensionLine [] dimensionLines = newDimensionLines.toArray(
5935           new DimensionLine [newDimensionLines.size()]);
5936       final Selectable [] oldSelection =
5937           oldSelectedItems.toArray(new Selectable [oldSelectedItems.size()]);
5938       final Level dimensionLinesLevel = this.home.getSelectedLevel();
5939       this.undoSupport.postEdit(new DimensionLinesCreationUndoableEdit(this, this.preferences,
5940           oldSelection, oldBasePlanLocked, oldAllLevelsSelection, dimensionLines,
5941           dimensionLinesLevel, newBasePlanLocked));
5942     }
5943   }
5944 
5945   /**
5946    * Undoable edit for dimension lines creation.
5947    */
5948   private static class DimensionLinesCreationUndoableEdit extends LocalizedUndoableEdit {
5949     private final PlanController   controller;
5950     private final Selectable []    oldSelection;
5951     private final boolean          oldBasePlanLocked;
5952     private final boolean          oldAllLevelsSelection;
5953     private final DimensionLine [] dimensionLines;
5954     private final Level            dimensionLinesLevel;
5955     private final boolean          newBasePlanLocked;
5956 
5957     public DimensionLinesCreationUndoableEdit(PlanController controller, UserPreferences preferences, Selectable [] oldSelection, boolean oldBasePlanLocked,
5958                                               boolean oldAllLevelsSelection, DimensionLine [] dimensionLines,
5959                                               Level dimensionLinesLevel, boolean newBasePlanLocked) {
5960       super(preferences, PlanController.class, "undoCreateDimensionLinesName");
5961       this.controller = controller;
5962       this.oldSelection = oldSelection;
5963       this.oldBasePlanLocked = oldBasePlanLocked;
5964       this.oldAllLevelsSelection = oldAllLevelsSelection;
5965       this.dimensionLines = dimensionLines;
5966       this.dimensionLinesLevel = dimensionLinesLevel;
5967       this.newBasePlanLocked = newBasePlanLocked;
5968     }
5969 
5970     @Override
5971     public void undo() throws CannotUndoException {
5972       super.undo();
5973       this.controller.doDeleteDimensionLines(this.dimensionLines, this.oldBasePlanLocked);
5974       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.oldAllLevelsSelection);
5975     }
5976 
5977     @Override
5978     public void redo() throws CannotRedoException {
5979       super.redo();
5980       this.controller.doAddDimensionLines(this.dimensionLines, null, this.dimensionLinesLevel, this.newBasePlanLocked);
5981       this.controller.selectAndShowItems(Arrays.asList(this.dimensionLines), false);
5982     }
5983   }
5984 
5985   /**
5986    * Adds the dimension lines in <code>dimensionLines</code> to plan component.
5987    */
5988   private void doAddDimensionLines(DimensionLine [] dimensionLines,
5989                                    Level [] dimensionLinesLevels,
5990                                    Level uniqueDimensionLinesLevel, boolean basePlanLocked) {
5991     for (int i = 0; i < dimensionLines.length; i++) {
5992       DimensionLine dimensionLine = dimensionLines [i];
5993       this.home.addDimensionLine(dimensionLine);
5994       dimensionLine.setLevel(dimensionLinesLevels != null
5995           ? dimensionLinesLevels [i]
5996           : uniqueDimensionLinesLevel);
5997     }
5998     this.home.setBasePlanLocked(basePlanLocked);
5999   }
6000 
6001   /**
6002    * Deletes dimension lines in <code>dimensionLines</code>.
6003    */
6004   private void doDeleteDimensionLines(DimensionLine [] dimensionLines,
6005                                       boolean basePlanLocked) {
6006     for (DimensionLine dimensionLine : dimensionLines) {
6007       this.home.deleteDimensionLine(dimensionLine);
6008     }
6009     this.home.setBasePlanLocked(basePlanLocked);
6010   }
6011 
6012   /**
6013    * Add <code>newPolylines</code> to home and post an undoable new polyline line operation.
6014    */
6015   public void addPolylines(List<Polyline> polylines) {
6016     final Polyline [] newPolylines = polylines.toArray(new Polyline [polylines.size()]);
6017     // Get indices of polylines added to home
6018     final int [] polylinesIndex = new int [polylines.size()];
6019     int endIndex = home.getPolylines().size();
6020     for (int i = 0; i < polylinesIndex.length; i++) {
6021       polylinesIndex [i] = endIndex++;
6022       this.home.addPolyline(newPolylines [i], polylinesIndex [i]);
6023     }
6024     postCreatePolylines(newPolylines, polylinesIndex, this.home.getSelectedItems(),
6025         this.home.isBasePlanLocked(), this.home.isAllLevelsSelection());
6026   }
6027 
6028   /**
6029    * Posts an undoable new polyline operation about <code>newPolylines</code>.
6030    */
6031   private void postCreatePolylines(final Polyline [] newPolylines,
6032                                    final int [] polylinesIndex,
6033                                    List<Selectable> oldSelectedItems,
6034                                    final boolean oldBasePlanLocked,
6035                                    final boolean oldAllLevelsSelection) {
6036     if (newPolylines.length > 0) {
6037       boolean basePlanLocked = this.home.isBasePlanLocked();
6038       if (basePlanLocked) {
6039         for (Polyline polyline : newPolylines) {
6040           // Unlock base plan if polyline is a part of it
6041           basePlanLocked &= !isItemPartOfBasePlan(polyline);
6042         }
6043         this.home.setBasePlanLocked(basePlanLocked);
6044       }
6045       final boolean newBasePlanLocked = basePlanLocked;
6046 
6047       final Selectable [] oldSelection =
6048           oldSelectedItems.toArray(new Selectable [oldSelectedItems.size()]);
6049       final Level polylinesLevel = this.home.getSelectedLevel();
6050       this.undoSupport.postEdit(new PolylinesCreationUndoableEdit(this, this.preferences,
6051           oldSelection, oldBasePlanLocked, oldAllLevelsSelection,
6052           newPolylines, polylinesIndex, polylinesLevel, newBasePlanLocked));
6053     }
6054   }
6055 
6056   /**
6057    * Undoable edit for polylines creation.
6058    */
6059   private static class PolylinesCreationUndoableEdit extends LocalizedUndoableEdit {
6060     private final PlanController controller;
6061     private final Selectable []  oldSelection;
6062     private final boolean        oldBasePlanLocked;
6063     private final boolean        oldAllLevelsSelection;
6064     private final Polyline []    newPolylines;
6065     private final int []         polylinesIndex;
6066     private final Level          polylinesLevel;
6067     private final boolean        newBasePlanLocked;
6068 
6069     public PolylinesCreationUndoableEdit(PlanController controller, UserPreferences preferences,
6070                                          Selectable [] oldSelection, boolean oldBasePlanLocked, boolean oldAllLevelsSelection,
6071                                          Polyline [] newPolylines, int [] polylinesIndex,
6072                                          Level polylinesLevel, boolean newBasePlanLocked) {
6073       super(preferences, PlanController.class, "undoCreatePolylinesName");
6074       this.controller = controller;
6075       this.oldSelection = oldSelection;
6076       this.oldBasePlanLocked = oldBasePlanLocked;
6077       this.oldAllLevelsSelection = oldAllLevelsSelection;
6078       this.newPolylines = newPolylines;
6079       this.polylinesIndex = polylinesIndex;
6080       this.polylinesLevel = polylinesLevel;
6081       this.newBasePlanLocked = newBasePlanLocked;
6082     }
6083 
6084     @Override
6085     public void undo() throws CannotUndoException {
6086       super.undo();
6087       this.controller.doDeletePolylines(this.newPolylines, this.oldBasePlanLocked);
6088       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.oldAllLevelsSelection);
6089     }
6090 
6091     @Override
6092     public void redo() throws CannotRedoException {
6093       super.redo();
6094       this.controller.doAddPolylines(this.newPolylines, this.polylinesIndex, null, this.polylinesLevel, this.newBasePlanLocked);
6095       this.controller.selectAndShowItems(Arrays.asList(this.newPolylines));
6096     }
6097   }
6098 
6099   /**
6100    * Posts an undoable new polyline operation about <code>newPolylines</code>.
6101    */
6102   private void postCreatePolylines(List<Polyline> polylines,
6103                                    List<Selectable> oldSelection,
6104                                    boolean basePlanLocked,
6105                                    boolean allLevelsSelection) {
6106     // Search the index of polylines in home list of polylines
6107     Polyline [] newPolylines = polylines.toArray(new Polyline [polylines.size()]);
6108     int [] polylinesIndex = new int [polylines.size()];
6109     List<Polyline> homePolylines = home.getPolylines();
6110     for (int i = 0; i < polylinesIndex.length; i++) {
6111       polylinesIndex [i] = homePolylines.lastIndexOf(newPolylines [i]);
6112     }
6113     postCreatePolylines(newPolylines, polylinesIndex, oldSelection, basePlanLocked, allLevelsSelection);
6114   }
6115 
6116   /**
6117    * Adds the <code>polylines</code> to plan component.
6118    */
6119   private void doAddPolylines(Polyline [] polylines,
6120                               int [] polylinesIndex,
6121                               Level [] polylinesLevels,
6122                               Level uniqueDimensionLinesLevel,
6123                               boolean basePlanLocked) {
6124     for (int i = 0; i < polylinesIndex.length; i++) {
6125       this.home.addPolyline(polylines [i], polylinesIndex [i]);
6126       polylines [i].setLevel(polylinesLevels != null
6127           ? polylinesLevels [i]
6128           : uniqueDimensionLinesLevel);
6129     }
6130     this.home.setBasePlanLocked(basePlanLocked);
6131   }
6132 
6133   /**
6134    * Deletes <code>polylines</code>.
6135    */
6136   private void doDeletePolylines(Polyline [] polylines,
6137                                  boolean basePlanLocked) {
6138     for (Polyline polyline : polylines) {
6139       this.home.deletePolyline(polyline);
6140     }
6141     this.home.setBasePlanLocked(basePlanLocked);
6142   }
6143 
6144 /**
6145    * Add <code>labels</code> to home and post an undoable new label operation.
6146    */
6147   public void addLabels(List<Label> labels) {
6148     for (Label label : labels) {
6149       this.home.addLabel(label);
6150     }
6151     postCreateLabels(labels, this.home.getSelectedItems(),
6152         this.home.isBasePlanLocked(), this.home.isAllLevelsSelection());
6153   }
6154 
6155   /**
6156    * Posts an undoable new label operation, about <code>newLabels</code>.
6157    */
6158   private void postCreateLabels(List<Label> newLabels,
6159                                 List<Selectable> oldSelectedItems,
6160                                 final boolean oldBasePlanLocked,
6161                                 final boolean oldAllLevelsSelection) {
6162     if (newLabels.size() > 0) {
6163       boolean basePlanLocked = this.home.isBasePlanLocked();
6164       if (basePlanLocked) {
6165         for (Label label : newLabels) {
6166           // Unlock base plan if label is a part of it
6167           basePlanLocked &= !isItemPartOfBasePlan(label);
6168         }
6169         this.home.setBasePlanLocked(basePlanLocked);
6170       }
6171       final boolean newBasePlanLocked = basePlanLocked;
6172 
6173       final Label [] labels = newLabels.toArray(new Label [newLabels.size()]);
6174       final Selectable [] oldSelection =
6175           oldSelectedItems.toArray(new Selectable [oldSelectedItems.size()]);
6176       final Level labelsLevel = this.home.getSelectedLevel();
6177       this.undoSupport.postEdit(new LabelsCreationUndoableEdit(this, this.preferences,
6178           oldSelection, oldBasePlanLocked, oldAllLevelsSelection,
6179           labels, labelsLevel, newBasePlanLocked));
6180     }
6181   }
6182 
6183   /**
6184    * Undoable edit for label creation.
6185    */
6186   private static class LabelsCreationUndoableEdit extends LocalizedUndoableEdit {
6187     private final PlanController controller;
6188     private final Selectable []  oldSelection;
6189     private final boolean        oldBasePlanLocked;
6190     private final boolean        oldAllLevelsSelection;
6191     private final Label []       labels;
6192     private final Level          labelsLevel;
6193     private final boolean        newBasePlanLocked;
6194 
6195     public LabelsCreationUndoableEdit(PlanController controller, UserPreferences preferences,
6196                                       Selectable [] oldSelection, boolean oldBasePlanLocked, boolean oldAllLevelsSelection,
6197                                       Label [] labels, Level labelsLevel, boolean newBasePlanLocked) {
6198       super(preferences, PlanController.class, "undoCreateLabelsName");
6199       this.controller = controller;
6200       this.oldSelection = oldSelection;
6201       this.oldBasePlanLocked = oldBasePlanLocked;
6202       this.oldAllLevelsSelection = oldAllLevelsSelection;
6203       this.labels = labels;
6204       this.labelsLevel = labelsLevel;
6205       this.newBasePlanLocked = newBasePlanLocked;
6206     }
6207 
6208     @Override
6209     public void undo() throws CannotUndoException {
6210       super.undo();
6211       this.controller.doDeleteLabels(this.labels, this.oldBasePlanLocked);
6212       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.oldAllLevelsSelection);
6213     }
6214 
6215     @Override
6216     public void redo() throws CannotRedoException {
6217       super.redo();
6218       this.controller.doAddLabels(this.labels, null, this.labelsLevel, this.newBasePlanLocked);
6219       this.controller.selectAndShowItems(Arrays.asList(this.labels), false);
6220     }
6221   }
6222 
6223   /**
6224    * Adds the labels in <code>labels</code> to plan component.
6225    */
6226   private void doAddLabels(Label [] labels, Level [] labelsLevels, Level uniqueLabelLevel, boolean basePlanLocked) {
6227     for (int i = 0; i < labels.length; i++) {
6228       Label label = labels [i];
6229       this.home.addLabel(label);
6230       label.setLevel(labelsLevels != null
6231           ? labelsLevels [i]
6232           : uniqueLabelLevel);
6233     }
6234     this.home.setBasePlanLocked(basePlanLocked);
6235   }
6236 
6237   /**
6238    * Deletes labels in <code>labels</code>.
6239    */
6240   private void doDeleteLabels(Label [] labels, boolean basePlanLocked) {
6241     for (Label label : labels) {
6242       this.home.deleteLabel(label);
6243     }
6244     this.home.setBasePlanLocked(basePlanLocked);
6245   }
6246 
6247   /**
6248    * Posts an undoable operation about <code>label</code> angle change.
6249    */
6250   private void postLabelRotation(final Label label, final float oldAngle) {
6251     final float newAngle = label.getAngle();
6252     if (newAngle != oldAngle) {
6253       this.undoSupport.postEdit(new LabelRotationUndoableEdit(this, this.preferences,
6254           oldAngle, label, newAngle));
6255     }
6256   }
6257 
6258   /**
6259    * Undoable edit for label rotation.
6260    */
6261   private static class LabelRotationUndoableEdit extends LocalizedUndoableEdit {
6262     private final PlanController controller;
6263     private final float          oldAngle;
6264     private final Label          label;
6265     private final float          newAngle;
6266 
6267     public LabelRotationUndoableEdit(PlanController controller, UserPreferences preferences,
6268                                      float oldAngle, Label label, float newAngle) {
6269       super(preferences, PlanController.class, "undoLabelRotationName");
6270       this.controller = controller;
6271       this.oldAngle = oldAngle;
6272       this.label = label;
6273       this.newAngle = newAngle;
6274     }
6275 
6276     @Override
6277     public void undo() throws CannotUndoException {
6278       super.undo();
6279       this.label.setAngle(this.oldAngle);
6280       this.controller.selectAndShowItems(Arrays.asList(new Label [] {this.label}));
6281     }
6282 
6283     @Override
6284     public void redo() throws CannotRedoException {
6285       super.redo();
6286       this.label.setAngle(this.newAngle);
6287       this.controller.selectAndShowItems(Arrays.asList(new Label [] {this.label}));
6288     }
6289   }
6290 
6291   /**
6292    * Post to undo support an elevation change on <code>label</code>.
6293    */
6294   private void postLabelElevation(final Label label, final float oldElevation) {
6295     final float newElevation = label.getElevation();
6296     if (newElevation != oldElevation) {
6297       this.undoSupport.postEdit(new LabelElevationModificationUndoableEdit(this, this.preferences,
6298           oldElevation, label, newElevation));
6299     }
6300   }
6301 
6302   /**
6303    * Undoable edit for label elevation modification.
6304    */
6305   private static class LabelElevationModificationUndoableEdit extends LocalizedUndoableEdit {
6306     private final PlanController controller;
6307     private final float          oldElevation;
6308     private final Label          label;
6309     private final float          newElevation;
6310 
6311     public LabelElevationModificationUndoableEdit(PlanController controller, UserPreferences preferences,
6312                                                   float oldElevation, Label label, float newElevation) {
6313       super(preferences, PlanController.class,
6314           oldElevation < newElevation
6315               ? "undoLabelRaiseName"
6316               : "undoLabelLowerName");
6317       this.controller = controller;
6318       this.oldElevation = oldElevation;
6319       this.label = label;
6320       this.newElevation = newElevation;
6321     }
6322 
6323     @Override
6324     public void undo() throws CannotUndoException {
6325       super.undo();
6326       this.label.setElevation(this.oldElevation);
6327       this.controller.selectAndShowItems(Arrays.asList(new Label [] {this.label}));
6328     }
6329 
6330     @Override
6331     public void redo() throws CannotRedoException {
6332       super.redo();
6333       this.label.setElevation(this.newElevation);
6334       this.controller.selectAndShowItems(Arrays.asList(new Label [] {this.label}));
6335     }
6336   }
6337 
6338   /**
6339    * Posts an undoable operation of a (<code>dx</code>, <code>dy</code>) move
6340    * of <code>movedItems</code>.
6341    */
6342   private void postItemsMove(List<? extends Selectable> movedItems,
6343                              List<? extends Selectable> oldSelectedItems,
6344                              final float dx, final float dy) {
6345     if (dx != 0 || dy != 0) {
6346       // Store the moved items in an array
6347       final Selectable [] itemsArray =
6348           movedItems.toArray(new Selectable [movedItems.size()]);
6349       final boolean allLevelsSelection = home.isAllLevelsSelection();
6350       final Selectable [] oldSelection =
6351           oldSelectedItems.toArray(new Selectable [oldSelectedItems.size()]);
6352       this.undoSupport.postEdit(new ItemsMovingUndoableEdit(this, this.preferences,
6353           oldSelection, allLevelsSelection, itemsArray, dx, dy));
6354     }
6355   }
6356 
6357   /**
6358    * Undoable edit for moving items.
6359    */
6360   private static class ItemsMovingUndoableEdit extends LocalizedUndoableEdit {
6361     private final PlanController controller;
6362     private final Selectable []  oldSelection;
6363     private final boolean        allLevelsSelection;
6364     private final Selectable []  itemsArray;
6365     private final float          dx;
6366     private final float          dy;
6367 
6368     public ItemsMovingUndoableEdit(PlanController controller, UserPreferences preferences,
6369                                    Selectable [] oldSelection, boolean allLevelsSelection,
6370                                    Selectable [] items, float dx, float dy) {
6371       super(preferences, PlanController.class, "undoMoveSelectionName");
6372       this.controller = controller;
6373       this.oldSelection = oldSelection;
6374       this.allLevelsSelection = allLevelsSelection;
6375       this.itemsArray = items;
6376       this.dx = dx;
6377       this.dy = dy;
6378     }
6379 
6380     @Override
6381     public void undo() throws CannotUndoException {
6382       super.undo();
6383       this.controller.doMoveAndShowItems(this.itemsArray, this.oldSelection, -this.dx, -this.dy, this.allLevelsSelection);
6384     }
6385 
6386     @Override
6387     public void redo() throws CannotRedoException {
6388       super.redo();
6389       this.controller.doMoveAndShowItems(this.itemsArray, this.itemsArray, this.dx, this.dy, this.allLevelsSelection);
6390     }
6391   }
6392 
6393   /**
6394    * Moves <code>movedItems</code> of (<code>dx</code>, <code>dy</code>) pixels,
6395    * selects them and make them visible.
6396    */
6397   private void doMoveAndShowItems(Selectable [] movedItems,
6398                                   Selectable [] selectedItems,
6399                                   float dx, float dy,
6400                                   boolean allLevelsSelection) {
6401     this.home.setAllLevelsSelection(allLevelsSelection);
6402     moveItems(Arrays.asList(movedItems), dx, dy);
6403     selectAndShowItems(Arrays.asList(selectedItems), allLevelsSelection);
6404   }
6405 
6406   /**
6407    * Posts an undoable operation of a (<code>dx</code>, <code>dy</code>) move
6408    * of the given <code>piece</code>.
6409    */
6410   private void postPieceOfFurnitureMove(final HomePieceOfFurniture piece,
6411                                         final float dx, final float dy,
6412                                         final float oldAngle,
6413                                         final float oldDepth,
6414                                         final float oldElevation,
6415                                         final boolean oldDoorOrWindowBoundToWall) {
6416     final float newAngle = piece.getAngle();
6417     final float newDepth = piece.getDepth();
6418     final float newElevation = piece.getElevation();
6419     if (dx != 0 || dy != 0
6420         || newAngle != oldAngle
6421         || newDepth != oldDepth
6422         || newElevation != oldElevation) {
6423       this.undoSupport.postEdit(new PieceOfFurnitureMovingUndoableEdit(this, this.preferences,
6424           oldAngle, oldDepth, oldElevation, oldDoorOrWindowBoundToWall,
6425           piece, dx, dy, newAngle, newDepth, newElevation));
6426     }
6427   }
6428 
6429   /**
6430    * Undoable edit for moving a piece of furniture.
6431    */
6432   private static class PieceOfFurnitureMovingUndoableEdit extends LocalizedUndoableEdit {
6433     private final PlanController       controller;
6434     private final float                oldAngle;
6435     private final float                oldDepth;
6436     private final float                oldElevation;
6437     private final boolean              oldDoorOrWindowBoundToWall;
6438     private final HomePieceOfFurniture piece;
6439     private final float                dx;
6440     private final float                dy;
6441     private final float                newAngle;
6442     private final float                newDepth;
6443     private final float                newElevation;
6444 
6445     public PieceOfFurnitureMovingUndoableEdit(PlanController controller, UserPreferences preferences,
6446                                               float oldAngle, float oldDepth, float oldElevation, boolean oldDoorOrWindowBoundToWall,
6447                                               HomePieceOfFurniture piece, float dx, float dy,
6448                                               float newAngle, float newDepth, float newElevation) {
6449       super(preferences, PlanController.class, "undoMoveSelectionName");
6450       this.controller = controller;
6451       this.oldAngle = oldAngle;
6452       this.oldDepth = oldDepth;
6453       this.oldElevation = oldElevation;
6454       this.oldDoorOrWindowBoundToWall = oldDoorOrWindowBoundToWall;
6455       this.piece = piece;
6456       this.dx = dx;
6457       this.dy = dy;
6458       this.newAngle = newAngle;
6459       this.newDepth = newDepth;
6460       this.newElevation = newElevation;
6461     }
6462 
6463     @Override
6464     public void undo() throws CannotUndoException {
6465       super.undo();
6466       this.piece.move(-this.dx, -this.dy);
6467       this.piece.setAngle(this.oldAngle);
6468       if (this.piece instanceof HomeDoorOrWindow
6469           && this.piece.isResizable()
6470           && this.controller.isItemResizable(this.piece)) {
6471         // Update of depth may happen only for doors and windows which can't be rotated around horizontal axes
6472         this.piece.setDepth(this.oldDepth);
6473       }
6474       this.piece.setElevation(this.oldElevation);
6475       if (this.piece instanceof HomeDoorOrWindow) {
6476         ((HomeDoorOrWindow)this.piece).setBoundToWall(this.oldDoorOrWindowBoundToWall);
6477       }
6478       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
6479     }
6480 
6481     @Override
6482     public void redo() throws CannotRedoException {
6483       super.redo();
6484       this.piece.move(this.dx, this.dy);
6485       this.piece.setAngle(this.newAngle);
6486       if (this.piece instanceof HomeDoorOrWindow
6487           && this.piece.isResizable()
6488           && this.controller.isItemResizable(this.piece)) {
6489         // Update of depth may happen only for doors and windows which can't be rotated around horizontal axes
6490         this.piece.setDepth(this.newDepth);
6491       }
6492       this.piece.setElevation(this.newElevation);
6493       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
6494     }
6495   }
6496 
6497   /**
6498    * Posts an undoable operation about duplication <code>items</code>.
6499    */
6500   private void postItemsDuplication(final List<Selectable> items,
6501                                     final List<Selectable> oldSelection) {
6502     boolean basePlanLocked = this.home.isBasePlanLocked();
6503     final boolean allLevelsSelection = this.home.isAllLevelsSelection();
6504     // Delete furniture and add it again in a compound edit
6505     List<HomePieceOfFurniture> furniture = Home.getFurnitureSubList(items);
6506     for (HomePieceOfFurniture piece : furniture) {
6507       this.home.deletePieceOfFurniture(piece);
6508     }
6509 
6510     // Post duplicated items in a compound edit
6511     this.undoSupport.beginUpdate();
6512     // Add a undoable edit that will select previous items at undo
6513     this.undoSupport.postEdit(new DuplicationStartUndoableEdit(this,
6514         oldSelection.toArray(new Selectable [oldSelection.size()]), allLevelsSelection));
6515 
6516     addFurniture(furniture);
6517     List<Selectable> emptyList = Collections.emptyList();
6518     postCreateWalls(Home.getWallsSubList(items), emptyList, basePlanLocked, allLevelsSelection);
6519     postCreateRooms(Home.getRoomsSubList(items), emptyList, basePlanLocked, allLevelsSelection);
6520     postCreatePolylines(Home.getPolylinesSubList(items), emptyList, basePlanLocked, allLevelsSelection);
6521     postCreateDimensionLines(Home.getDimensionLinesSubList(items), emptyList, basePlanLocked, allLevelsSelection);
6522     postCreateLabels(Home.getLabelsSubList(items), emptyList, basePlanLocked, allLevelsSelection);
6523 
6524     // Add a undoable edit that will select all the items at redo
6525     this.undoSupport.postEdit(new DuplicationEndUndoableEdit(this, this.preferences, items.toArray(new Selectable [items.size()])));
6526 
6527     // End compound edit
6528     this.undoSupport.endUpdate();
6529 
6530     selectItems(items);
6531   }
6532 
6533   /**
6534    * Undoable edit for duplication start.
6535    */
6536   private static class DuplicationStartUndoableEdit extends AbstractUndoableEdit {
6537     private final PlanController controller;
6538     private final Selectable []  oldSelection;
6539     private final boolean        allLevelsSelection;
6540 
6541     public DuplicationStartUndoableEdit(PlanController controller, Selectable [] oldSelection, boolean allLevelsSelection) {
6542       this.controller = controller;
6543       this.oldSelection = oldSelection;
6544       this.allLevelsSelection = allLevelsSelection;
6545     }
6546 
6547     @Override
6548     public void undo() throws CannotRedoException {
6549       super.undo();
6550       this.controller.selectAndShowItems(Arrays.asList(this.oldSelection), this.allLevelsSelection);
6551     }
6552   }
6553 
6554   /**
6555    * Undoable edit for duplication end.
6556    */
6557   private static class DuplicationEndUndoableEdit extends LocalizedUndoableEdit {
6558     private final PlanController controller;
6559     private final Selectable []  items;
6560 
6561     public DuplicationEndUndoableEdit(PlanController controller, UserPreferences preferences, Selectable [] items) {
6562       super(preferences, PlanController.class, "undoDuplicateSelectionName");
6563       this.controller = controller;
6564       this.items = items;
6565     }
6566 
6567     @Override
6568     public void redo() throws CannotRedoException {
6569       super.redo();
6570       this.controller.selectAndShowItems(Arrays.asList(this.items));
6571     }
6572   }
6573 
6574   /**
6575    * Posts an undoable operation about <code>wall</code> resizing.
6576    */
6577   private void postWallResize(final Wall wall, final float oldX, final float oldY,
6578                               final boolean startPoint) {
6579     final float newX;
6580     final float newY;
6581     if (startPoint) {
6582       newX = wall.getXStart();
6583       newY = wall.getYStart();
6584     } else {
6585       newX = wall.getXEnd();
6586       newY = wall.getYEnd();
6587     }
6588     if (newX != oldX || newY != oldY) {
6589       this.undoSupport.postEdit(new WallResizingUndoableEdit(this, this.preferences,
6590           oldX, oldY, wall, startPoint, newX, newY));
6591     }
6592   }
6593 
6594   /**
6595    * Undoable edit for wall resizing.
6596    */
6597   private static class WallResizingUndoableEdit extends LocalizedUndoableEdit {
6598     private final PlanController controller;
6599     private final float          oldX;
6600     private final float          oldY;
6601     private final Wall           wall;
6602     private final boolean        startPoint;
6603     private final float          newX;
6604     private final float          newY;
6605 
6606     public WallResizingUndoableEdit(PlanController controller, UserPreferences preferences,
6607                                     float oldX, float oldY, Wall wall, boolean startPoint, float newX, float newY) {
6608       super(preferences, PlanController.class, "undoWallResizeName");
6609       this.controller = controller;
6610       this.oldX = oldX;
6611       this.oldY = oldY;
6612       this.wall = wall;
6613       this.startPoint = startPoint;
6614       this.newX = newX;
6615       this.newY = newY;
6616     }
6617 
6618     @Override
6619     public void undo() throws CannotUndoException {
6620       super.undo();
6621       moveWallPoint(this.wall, this.oldX, this.oldY, this.startPoint);
6622       this.controller.selectAndShowItems(Arrays.asList(new Wall [] {this.wall}));
6623     }
6624 
6625     @Override
6626     public void redo() throws CannotRedoException {
6627       super.redo();
6628       moveWallPoint(this.wall, this.newX, this.newY, this.startPoint);
6629       this.controller.selectAndShowItems(Arrays.asList(new Wall [] {this.wall}));
6630     }
6631   }
6632 
6633   /**
6634    * Posts an undoable operation about <code>wall</code> arc extent change.
6635    */
6636   private void postWallArcExtent(final Wall wall, final Float oldArcExtent) {
6637     final Float newArcExtent = wall.getArcExtent();
6638     if (newArcExtent != oldArcExtent
6639         && (newArcExtent == null || !newArcExtent.equals(oldArcExtent))) {
6640       this.undoSupport.postEdit(new WallArcExtentModificationUndoableEdit(
6641           this, this.preferences, oldArcExtent, wall, newArcExtent));
6642     }
6643   }
6644 
6645   /**
6646    * Undoable edit for wall arc extent modification.
6647    */
6648   private static class WallArcExtentModificationUndoableEdit extends LocalizedUndoableEdit {
6649     private final PlanController controller;
6650     private final Float          oldArcExtent;
6651     private final Wall           wall;
6652     private final Float          newArcExtent;
6653 
6654     public WallArcExtentModificationUndoableEdit(PlanController controller, UserPreferences preferences,
6655                                                  Float oldArcExtent, Wall wall, Float newArcExtent) {
6656       super(preferences, PlanController.class, "undoWallArcExtentName");
6657       this.controller = controller;
6658       this.oldArcExtent = oldArcExtent;
6659       this.wall = wall;
6660       this.newArcExtent = newArcExtent;
6661     }
6662 
6663     @Override
6664     public void undo() throws CannotUndoException {
6665       super.undo();
6666       this.wall.setArcExtent(this.oldArcExtent);
6667       this.controller.selectAndShowItems(Arrays.asList(new Wall [] {this.wall}));
6668     }
6669 
6670     @Override
6671     public void redo() throws CannotRedoException {
6672       super.redo();
6673       this.wall.setArcExtent(this.newArcExtent);
6674       this.controller.selectAndShowItems(Arrays.asList(new Wall [] {this.wall}));
6675     }
6676   }
6677 
6678   /**
6679    * Posts an undoable operation about <code>room</code> resizing.
6680    */
6681   private void postRoomResize(final Room room, final float oldX, final float oldY,
6682                               final int pointIndex) {
6683     float [] roomPoint = room.getPoints() [pointIndex];
6684     final float newX = roomPoint [0];
6685     final float newY = roomPoint [1];
6686     if (newX != oldX || newY != oldY) {
6687       UndoableEdit undoableEdit = new RoomResizingUndoableEdit(this, this.preferences,
6688           oldX, oldY, room, pointIndex, newX, newY);
6689       this.undoSupport.postEdit(undoableEdit);
6690     }
6691   }
6692 
6693   /**
6694    * Undoable edit for room resizing.
6695    */
6696   private static class RoomResizingUndoableEdit extends LocalizedUndoableEdit {
6697     private final PlanController controller;
6698     private final float          oldX;
6699     private final float          oldY;
6700     private final Room           room;
6701     private final int            pointIndex;
6702     private final float          newX;
6703     private final float          newY;
6704 
6705     public RoomResizingUndoableEdit(PlanController controller, UserPreferences preferences,
6706                                     float oldX, float oldY, Room room, int pointIndex, float newX, float newY) {
6707       super(preferences, PlanController.class, "undoRoomResizeName");
6708       this.controller = controller;
6709       this.oldX = oldX;
6710       this.oldY = oldY;
6711       this.room = room;
6712       this.pointIndex = pointIndex;
6713       this.newX = newX;
6714       this.newY = newY;
6715     }
6716 
6717     @Override
6718     public void undo() throws CannotUndoException {
6719       super.undo();
6720       moveRoomPoint(this.room, this.oldX, this.oldY, this.pointIndex);
6721       this.controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
6722     }
6723 
6724     @Override
6725     public void redo() throws CannotRedoException {
6726       super.redo();
6727       moveRoomPoint(this.room, this.newX, this.newY, this.pointIndex);
6728       this.controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
6729     }
6730   }
6731 
6732   /**
6733    * Posts an undoable operation about <code>room</code> name offset change.
6734    */
6735   private void postRoomNameOffset(final Room room, final float oldNameXOffset,
6736                                   final float oldNameYOffset) {
6737     final float newNameXOffset = room.getNameXOffset();
6738     final float newNameYOffset = room.getNameYOffset();
6739     if (newNameXOffset != oldNameXOffset
6740         || newNameYOffset != oldNameYOffset) {
6741       this.undoSupport.postEdit(new RoomNameOffsetModificationUndoableEdit(this, this.preferences,
6742           oldNameXOffset, oldNameYOffset, room, newNameXOffset, newNameYOffset));
6743     }
6744   }
6745 
6746   /**
6747    * Undoable edit for room name offset modification.
6748    */
6749   private static class RoomNameOffsetModificationUndoableEdit extends LocalizedUndoableEdit {
6750     private final PlanController controller;
6751     private final float oldNameXOffset;
6752     private final float oldNameYOffset;
6753     private final Room  room;
6754     private final float newNameXOffset;
6755     private final float newNameYOffset;
6756 
6757     public RoomNameOffsetModificationUndoableEdit(PlanController controller, UserPreferences preferences,
6758                                                   float oldNameXOffset, float oldNameYOffset,
6759                                                   Room room, float newNameXOffset, float newNameYOffset) {
6760       super(preferences, PlanController.class, "undoRoomNameOffsetName");
6761       this.controller = controller;
6762       this.oldNameXOffset = oldNameXOffset;
6763       this.oldNameYOffset = oldNameYOffset;
6764       this.room = room;
6765       this.newNameXOffset = newNameXOffset;
6766       this.newNameYOffset = newNameYOffset;
6767     }
6768 
6769     @Override
6770     public void undo() throws CannotUndoException {
6771       super.undo();
6772       this.room.setNameXOffset(this.oldNameXOffset);
6773       this.room.setNameYOffset(this.oldNameYOffset);
6774       this.controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
6775     }
6776 
6777     @Override
6778     public void redo() throws CannotRedoException {
6779       super.redo();
6780       this.room.setNameXOffset(this.newNameXOffset);
6781       this.room.setNameYOffset(this.newNameYOffset);
6782       this.controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
6783     }
6784   }
6785 
6786   /**
6787    * Posts an undoable operation about <code>room</code> name angle change.
6788    */
6789   private void postRoomNameRotation(final Room room, final float oldNameAngle) {
6790     final float newNameAngle = room.getNameAngle();
6791     if (newNameAngle != oldNameAngle) {
6792       this.undoSupport.postEdit(new RoomNameRotationUndoableEdit(this, this.preferences,
6793           oldNameAngle, room, newNameAngle));
6794     }
6795   }
6796 
6797   /**
6798    * Undoable edit for room name rotation.
6799    */
6800   private static class RoomNameRotationUndoableEdit extends LocalizedUndoableEdit {
6801     private final PlanController controller;
6802     private final float          oldNameAngle;
6803     private final Room           room;
6804     private final float          newNameAngle;
6805 
6806     public RoomNameRotationUndoableEdit(PlanController controller, UserPreferences preferences,
6807                                         float oldNameAngle, Room room, float newNameAngle) {
6808       super(preferences, PlanController.class, "undoRoomNameRotationName");
6809       this.controller = controller;
6810       this.oldNameAngle = oldNameAngle;
6811       this.room = room;
6812       this.newNameAngle = newNameAngle;
6813     }
6814 
6815     @Override
6816     public void undo() throws CannotUndoException {
6817       super.undo();
6818       this.room.setNameAngle(this.oldNameAngle);
6819       this.controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
6820     }
6821 
6822     @Override
6823     public void redo() throws CannotRedoException {
6824       super.redo();
6825       this.room.setNameAngle(this.newNameAngle);
6826       this.controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
6827     }
6828   }
6829 
6830   /**
6831    * Posts an undoable operation about <code>room</code> area offset change.
6832    */
6833   private void postRoomAreaOffset(final Room room, final float oldAreaXOffset,
6834                                   final float oldAreaYOffset) {
6835     final float newAreaXOffset = room.getAreaXOffset();
6836     final float newAreaYOffset = room.getAreaYOffset();
6837     if (newAreaXOffset != oldAreaXOffset
6838         || newAreaYOffset != oldAreaYOffset) {
6839       this.undoSupport.postEdit(new RoomAreaOffsetModificationUndoableEdit(this, this.preferences,
6840           oldAreaXOffset, oldAreaYOffset, room, newAreaXOffset, newAreaYOffset));
6841     }
6842   }
6843 
6844   /**
6845    * Undoable edit for room area offset modification.
6846    */
6847   private static class RoomAreaOffsetModificationUndoableEdit extends LocalizedUndoableEdit {
6848     private final PlanController controller;
6849     private final float          oldAreaXOffset;
6850     private final float          oldAreaYOffset;
6851     private final Room           room;
6852     private final float          newAreaXOffset;
6853     private final float          newAreaYOffset;
6854 
6855     public RoomAreaOffsetModificationUndoableEdit(PlanController controller, UserPreferences preferences,
6856                                                   float oldAreaXOffset, float oldAreaYOffset,
6857                                                   Room room, float newAreaXOffset, float newAreaYOffset) {
6858       super(preferences, PlanController.class, "undoRoomAreaOffsetName");
6859       this.controller = controller;
6860       this.oldAreaXOffset = oldAreaXOffset;
6861       this.oldAreaYOffset = oldAreaYOffset;
6862       this.room = room;
6863       this.newAreaXOffset = newAreaXOffset;
6864       this.newAreaYOffset = newAreaYOffset;
6865     }
6866 
6867     @Override
6868     public void undo() throws CannotUndoException {
6869       super.undo();
6870       this.room.setAreaXOffset(this.oldAreaXOffset);
6871       this.room.setAreaYOffset(this.oldAreaYOffset);
6872       this.controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
6873     }
6874 
6875     @Override
6876     public void redo() throws CannotRedoException {
6877       super.redo();
6878       this.room.setAreaXOffset(this.newAreaXOffset);
6879       this.room.setAreaYOffset(this.newAreaYOffset);
6880       this.controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
6881     }
6882   }
6883 
6884   /**
6885    * Posts an undoable operation about <code>room</code> area angle change.
6886    */
6887   private void postRoomAreaRotation(final Room room, final float oldAreaAngle) {
6888     final float newAreaAngle = room.getAreaAngle();
6889     if (newAreaAngle != oldAreaAngle) {
6890       this.undoSupport.postEdit(new RoomAreaRotationUndoableEdit(this, this.preferences,
6891           oldAreaAngle, room, newAreaAngle));
6892     }
6893   }
6894 
6895   /**
6896    * Undoable edit for room area rotation.
6897    */
6898   private static class RoomAreaRotationUndoableEdit extends LocalizedUndoableEdit {
6899     private final PlanController controller;
6900     private final float oldAreaAngle;
6901     private final Room  room;
6902     private final float newAreaAngle;
6903 
6904     public RoomAreaRotationUndoableEdit(PlanController controller, UserPreferences preferences,
6905                                         float oldAreaAngle, Room room, float newAreaAngle) {
6906       super(preferences, PlanController.class, "undoRoomAreaRotationName");
6907       this.controller = controller;
6908       this.oldAreaAngle = oldAreaAngle;
6909       this.room = room;
6910       this.newAreaAngle = newAreaAngle;
6911     }
6912 
6913     @Override
6914     public void undo() throws CannotUndoException {
6915       super.undo();
6916       this.room.setAreaAngle(this.oldAreaAngle);
6917       controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
6918     }
6919 
6920     @Override
6921     public void redo() throws CannotRedoException {
6922       super.redo();
6923       this.room.setAreaAngle(this.newAreaAngle);
6924       controller.selectAndShowItems(Arrays.asList(new Room [] {this.room}));
6925     }
6926   }
6927 
6928   /**
6929    * Post to undo support an angle change on <code>piece</code>.
6930    */
6931   private void postPieceOfFurnitureRotation(final HomePieceOfFurniture piece,
6932                                             final float oldAngle,
6933                                             final boolean oldDoorOrWindowBoundToWall) {
6934     final float newAngle = piece.getAngle();
6935     if (newAngle != oldAngle) {
6936       this.undoSupport.postEdit(new PieceOfFurnitureRotationUndoableEdit(this, this.preferences,
6937           oldAngle, oldDoorOrWindowBoundToWall, piece, newAngle));
6938     }
6939   }
6940 
6941   /**
6942    * Undoable edit for the rotation of a piece of furniture.
6943    */
6944   private static class PieceOfFurnitureRotationUndoableEdit extends LocalizedUndoableEdit {
6945     private final PlanController       controller;
6946     private final float                oldAngle;
6947     private final boolean              oldDoorOrWindowBoundToWall;
6948     private final HomePieceOfFurniture piece;
6949     private final float                newAngle;
6950 
6951     public PieceOfFurnitureRotationUndoableEdit(PlanController controller, UserPreferences preferences,
6952                                                 float oldAngle, boolean oldDoorOrWindowBoundToWall,
6953                                                 HomePieceOfFurniture piece, float newAngle) {
6954       super(preferences, PlanController.class, "undoPieceOfFurnitureRotationName");
6955       this.controller = controller;
6956       this.oldAngle = oldAngle;
6957       this.oldDoorOrWindowBoundToWall = oldDoorOrWindowBoundToWall;
6958       this.piece = piece;
6959       this.newAngle = newAngle;
6960     }
6961 
6962     @Override
6963     public void undo() throws CannotUndoException {
6964       super.undo();
6965       this.piece.setAngle(this.oldAngle);
6966       if (this.piece instanceof HomeDoorOrWindow) {
6967         ((HomeDoorOrWindow)this.piece).setBoundToWall(this.oldDoorOrWindowBoundToWall);
6968       }
6969       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
6970     }
6971 
6972     @Override
6973     public void redo() throws CannotRedoException {
6974       super.redo();
6975       this.piece.setAngle(this.newAngle);
6976       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
6977     }
6978   }
6979 
6980   /**
6981    * Post to undo support a pitch change on <code>piece</code>.
6982    */
6983   private void postPieceOfFurniturePitchRotation(final HomePieceOfFurniture piece, float oldPitch,
6984                                                  float oldWidthInPlan, float oldDepthInPlan, float oldHeightInPlan) {
6985     final float newPitch = piece.getPitch();
6986     if (newPitch != oldPitch) {
6987       this.undoSupport.postEdit(new PieceOfFurniturePitchRotationUndoableEdit(this, this.preferences,
6988           oldPitch, oldWidthInPlan, oldDepthInPlan, oldHeightInPlan, piece,
6989           newPitch, piece.getWidthInPlan(), piece.getDepthInPlan(), piece.getHeightInPlan()));
6990     }
6991   }
6992 
6993   /**
6994    * Undoable edit for the pitch rotation of a piece of furniture.
6995    */
6996   private static class PieceOfFurniturePitchRotationUndoableEdit extends LocalizedUndoableEdit {
6997     private final PlanController       controller;
6998     private final float                oldPitch;
6999     private final float                oldWidthInPlan;
7000     private final float                oldDepthInPlan;
7001     private final float                oldHeightInPlan;
7002     private final HomePieceOfFurniture piece;
7003     private final float                newPitch;
7004     private final float                newWidthInPlan;
7005     private final float                newDepthInPlan;
7006     private final float                newHeightInPlan;
7007 
7008     public PieceOfFurniturePitchRotationUndoableEdit(PlanController controller, UserPreferences preferences,
7009                                                      float oldPitch, float oldWidthInPlan, float oldDepthInPlan, float oldHeightInPlan,
7010                                                      HomePieceOfFurniture piece, float newPitch, float newWidthInPlan, float newDepthInPlan, float newHeightInPlan) {
7011       super(preferences, PlanController.class, "undoPieceOfFurnitureRotationName");
7012       this.controller = controller;
7013       this.oldPitch = oldPitch;
7014       this.oldWidthInPlan = oldWidthInPlan;
7015       this.oldDepthInPlan = oldDepthInPlan;
7016       this.oldHeightInPlan = oldHeightInPlan;
7017       this.piece = piece;
7018       this.newPitch = newPitch;
7019       this.newWidthInPlan = newWidthInPlan;
7020       this.newDepthInPlan = newDepthInPlan;
7021       this.newHeightInPlan = newHeightInPlan;
7022     }
7023 
7024     @Override
7025     public void undo() throws CannotUndoException {
7026       super.undo();
7027       this.controller.setPieceOfFurniturePitch(this.piece, this.oldPitch, this.oldWidthInPlan, this.oldDepthInPlan, this.oldHeightInPlan);
7028       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
7029     }
7030 
7031     @Override
7032     public void redo() throws CannotRedoException {
7033       super.redo();
7034       this.controller.setPieceOfFurniturePitch(this.piece, this.newPitch, this.newWidthInPlan, this.newDepthInPlan, this.newHeightInPlan);
7035       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
7036     }
7037   }
7038 
7039   /**
7040    * Sets the pitch angle on the given piece without computing new size in plan.
7041    */
7042   private void setPieceOfFurniturePitch(HomePieceOfFurniture piece, float pitch,
7043                                         float widthInPlan, float depthInPlan, float heightInPlan) {
7044     piece.removePropertyChangeListener(this.furnitureSizeChangeListener);
7045     piece.setPitch(pitch);
7046     piece.setWidthInPlan(widthInPlan);
7047     piece.setDepthInPlan(depthInPlan);
7048     piece.setHeightInPlan(heightInPlan);
7049     piece.addPropertyChangeListener(this.furnitureSizeChangeListener);
7050   }
7051 
7052   /**
7053    * Post to undo support a roll change on <code>piece</code>.
7054    */
7055   private void postPieceOfFurnitureRollRotation(final HomePieceOfFurniture piece, float oldRoll,
7056                                                 float oldWidthInPlan, float oldDepthInPlan, float oldHeightInPlan) {
7057     final float newRoll = piece.getRoll();
7058     if (newRoll != oldRoll) {
7059       this.undoSupport.postEdit(new PieceOfFurnitureRollRotationUndoableEdit(this, this.preferences,
7060           oldRoll, oldWidthInPlan, oldDepthInPlan, oldHeightInPlan, piece,
7061           newRoll, piece.getWidthInPlan(), piece.getDepthInPlan(), piece.getHeightInPlan()));
7062     }
7063   }
7064 
7065  /**
7066    * Undoable edit for the roll rotation of a piece of furniture.
7067    */
7068   private static class PieceOfFurnitureRollRotationUndoableEdit extends LocalizedUndoableEdit {
7069     private final PlanController       controller;
7070     private final float                oldRoll;
7071     private final float                oldWidthInPlan;
7072     private final float                oldDepthInPlan;
7073     private final float                oldHeightInPlan;
7074     private final HomePieceOfFurniture piece;
7075     private final float                newRoll;
7076     private final float                newWidthInPlan;
7077     private final float                newDepthInPlan;
7078     private final float                newHeightInPlan;
7079 
7080     public PieceOfFurnitureRollRotationUndoableEdit(PlanController controller, UserPreferences preferences,
7081                                                     float oldRoll, float oldWidthInPlan, float oldDepthInPlan, float oldHeightInPlan,
7082                                                     HomePieceOfFurniture piece, float newRoll, float newWidthInPlan, float newDepthInPlan, float newHeightInPlan) {
7083       super(preferences, PlanController.class, "undoPieceOfFurnitureRotationName");
7084       this.controller = controller;
7085       this.oldRoll = oldRoll;
7086       this.oldWidthInPlan = oldWidthInPlan;
7087       this.oldDepthInPlan = oldDepthInPlan;
7088       this.oldHeightInPlan = oldHeightInPlan;
7089       this.piece = piece;
7090       this.newRoll = newRoll;
7091       this.newWidthInPlan = newWidthInPlan;
7092       this.newDepthInPlan = newDepthInPlan;
7093       this.newHeightInPlan = newHeightInPlan;
7094     }
7095 
7096     @Override
7097     public void undo() throws CannotUndoException {
7098       super.undo();
7099       this.controller.setPieceOfFurnitureRoll(this.piece, this.oldRoll, this.oldWidthInPlan, this.oldDepthInPlan, this.oldHeightInPlan);
7100       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
7101     }
7102 
7103     @Override
7104     public void redo() throws CannotRedoException {
7105       super.redo();
7106       this.controller.setPieceOfFurnitureRoll(this.piece, this.newRoll, this.newWidthInPlan, this.newDepthInPlan, this.newHeightInPlan);
7107       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
7108     }
7109   }
7110 
7111   /**
7112    * Sets the roll angle on the given piece without computing new size in plan.
7113    */
7114   private void setPieceOfFurnitureRoll(HomePieceOfFurniture piece, float roll,
7115                                        float widthInPlan, float depthInPlan, float heightInPlan) {
7116     piece.removePropertyChangeListener(this.furnitureSizeChangeListener);
7117     piece.setRoll(roll);
7118     piece.setWidthInPlan(widthInPlan);
7119     piece.setDepthInPlan(depthInPlan);
7120     piece.setHeightInPlan(heightInPlan);
7121     piece.addPropertyChangeListener(this.furnitureSizeChangeListener);
7122   }
7123 
7124   /**
7125    * Post to undo support an elevation change on <code>piece</code>.
7126    */
7127   private void postPieceOfFurnitureElevation(final HomePieceOfFurniture piece, final float oldElevation) {
7128     final float newElevation = piece.getElevation();
7129     if (newElevation != oldElevation) {
7130       this.undoSupport.postEdit(new PieceOfFurnitureElevationModificationUndoableEdit(this, this.preferences,
7131           oldElevation, piece, newElevation));
7132     }
7133   }
7134 
7135   /**
7136    * Undoable edit for the elevation modification of a piece of furniture.
7137    */
7138   private static class PieceOfFurnitureElevationModificationUndoableEdit extends LocalizedUndoableEdit {
7139     private final PlanController       controller;
7140     private final float                oldElevation;
7141     private final HomePieceOfFurniture piece;
7142     private final float                newElevation;
7143 
7144     public PieceOfFurnitureElevationModificationUndoableEdit(PlanController controller, UserPreferences preferences,
7145                                                              float oldElevation, HomePieceOfFurniture piece, float newElevation) {
7146       super(preferences, PlanController.class,
7147           oldElevation < newElevation
7148               ? "undoPieceOfFurnitureRaiseName"
7149               : "undoPieceOfFurnitureLowerName");
7150       this.controller = controller;
7151       this.oldElevation = oldElevation;
7152       this.piece = piece;
7153       this.newElevation = newElevation;
7154     }
7155 
7156     @Override
7157     public void undo() throws CannotUndoException {
7158       super.undo();
7159       this.piece.setElevation(this.oldElevation);
7160       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
7161     }
7162 
7163     @Override
7164     public void redo() throws CannotRedoException {
7165       super.redo();
7166       this.piece.setElevation(this.newElevation);
7167       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
7168     }
7169   }
7170 
7171   /**
7172    * Post to undo support a height change on <code>piece</code>.
7173    */
7174   private void postPieceOfFurnitureHeightResize(final ResizedPieceOfFurniture resizedPiece) {
7175     if (resizedPiece.getPieceOfFurniture().getHeight() != resizedPiece.getHeight()) {
7176       postPieceOfFurnitureResize(resizedPiece, "undoPieceOfFurnitureHeightResizeName");
7177     }
7178   }
7179 
7180   /**
7181    * Post to undo support a width and depth change on <code>piece</code>.
7182    */
7183   private void postPieceOfFurnitureWidthAndDepthResize(final ResizedPieceOfFurniture resizedPiece) {
7184     HomePieceOfFurniture piece = resizedPiece.getPieceOfFurniture();
7185     if (piece.getWidth() != resizedPiece.getWidth()
7186         || piece.getDepth() != resizedPiece.getDepth()) {
7187       postPieceOfFurnitureResize(resizedPiece, "undoPieceOfFurnitureWidthAndDepthResizeName");
7188     }
7189   }
7190 
7191   /**
7192    * Post to undo support a size change on <code>piece</code>.
7193    */
7194   private void postPieceOfFurnitureResize(final ResizedPieceOfFurniture resizedPiece,
7195                                           final String presentationNameKey) {
7196     HomePieceOfFurniture piece = resizedPiece.getPieceOfFurniture();
7197     final boolean doorOrWindowBoundToWall = piece instanceof HomeDoorOrWindow
7198         && ((HomeDoorOrWindow)piece).isBoundToWall();
7199     this.undoSupport.postEdit(new PieceOfFurnitureResizingUndoableEdit(this, this.preferences,
7200         presentationNameKey, doorOrWindowBoundToWall, resizedPiece,
7201         piece.getX(), piece.getY(), piece.getWidth(), piece.getDepth(), piece.getHeight()));
7202   }
7203 
7204   /**
7205    * Undoable edit for the resizing of a piece of furniture.
7206    */
7207   private static class PieceOfFurnitureResizingUndoableEdit extends LocalizedUndoableEdit {
7208     private final PlanController          controller;
7209     private final boolean                 doorOrWindowBoundToWall;
7210     private final ResizedPieceOfFurniture resizedPiece;
7211     private final float                   newX;
7212     private final float                   newY;
7213     private final float                   newWidth;
7214     private final float                   newDepth;
7215     private final float                   newHeight;
7216 
7217     public PieceOfFurnitureResizingUndoableEdit(PlanController controller, UserPreferences preferences, String presentationNameKey,
7218                                                 boolean doorOrWindowBoundToWall, ResizedPieceOfFurniture resizedPiece,
7219                                                 float newX, float newY, float newWidth, float newDepth, float newHeight) {
7220       super(preferences, PlanController.class, presentationNameKey);
7221       this.controller = controller;
7222       this.doorOrWindowBoundToWall = doorOrWindowBoundToWall;
7223       this.resizedPiece = resizedPiece;
7224       this.newX = newX;
7225       this.newY = newY;
7226       this.newWidth = newWidth;
7227       this.newDepth = newDepth;
7228       this.newHeight = newHeight;
7229     }
7230 
7231     @Override
7232     public void undo() throws CannotUndoException {
7233       super.undo();
7234       this.controller.resetPieceOfFurnitureSize(this.resizedPiece);
7235       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.resizedPiece.getPieceOfFurniture()}));
7236     }
7237 
7238     @Override
7239     public void redo() throws CannotRedoException {
7240       super.redo();
7241       HomePieceOfFurniture piece = this.resizedPiece.getPieceOfFurniture();
7242       piece.setX(this.newX);
7243       piece.setY(this.newY);
7244       this.controller.setPieceOfFurnitureSize(this.resizedPiece, this.newWidth, this.newDepth, this.newHeight);
7245       if (piece instanceof HomeDoorOrWindow) {
7246         ((HomeDoorOrWindow)piece).setBoundToWall(this.doorOrWindowBoundToWall);
7247       }
7248       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {piece}));
7249     }
7250   }
7251 
7252   /**
7253    * Sets the size of the given piece of furniture.
7254    */
7255   private void setPieceOfFurnitureSize(ResizedPieceOfFurniture resizedPiece,
7256                                        float width, float depth, float height) {
7257     HomePieceOfFurniture piece = resizedPiece.getPieceOfFurniture();
7258     piece.removePropertyChangeListener(this.furnitureSizeChangeListener);
7259     if (piece instanceof HomeFurnitureGroup) {
7260       for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
7261         childPiece.removePropertyChangeListener(this.furnitureSizeChangeListener);
7262       }
7263     }
7264     ResizedPieceOfFurniture.setPieceOfFurnitureSize(piece, width, depth, height);
7265     piece.addPropertyChangeListener(this.furnitureSizeChangeListener);
7266     if (piece instanceof HomeFurnitureGroup) {
7267       for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
7268         childPiece.addPropertyChangeListener(this.furnitureSizeChangeListener);
7269       }
7270     }
7271   }
7272 
7273   /**
7274    * Resets the size of the given piece of furniture.
7275    */
7276   private void resetPieceOfFurnitureSize(ResizedPieceOfFurniture resizedPiece) {
7277     HomePieceOfFurniture piece = resizedPiece.getPieceOfFurniture();
7278     piece.removePropertyChangeListener(this.furnitureSizeChangeListener);
7279     if (piece instanceof HomeFurnitureGroup) {
7280       for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
7281         childPiece.removePropertyChangeListener(this.furnitureSizeChangeListener);
7282       }
7283     }
7284     resizedPiece.reset();
7285     piece.addPropertyChangeListener(this.furnitureSizeChangeListener);
7286     if (piece instanceof HomeFurnitureGroup) {
7287       for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
7288         childPiece.addPropertyChangeListener(this.furnitureSizeChangeListener);
7289       }
7290     }
7291   }
7292 
7293   /**
7294    * Post to undo support a power modification on <code>light</code>.
7295    */
7296   private void postLightPowerModification(final HomeLight light, final float oldPower) {
7297     final float newPower = light.getPower();
7298     if (newPower != oldPower) {
7299       this.undoSupport.postEdit(new LightPowerModificationUndoableEdit(this, this.preferences,
7300           oldPower, light, newPower));
7301     }
7302   }
7303 
7304   /**
7305    * Undoable edit for the light power modification.
7306    */
7307   private static class LightPowerModificationUndoableEdit extends LocalizedUndoableEdit {
7308     private final PlanController controller;
7309     private final float          oldPower;
7310     private final HomeLight      light;
7311     private final float          newPower;
7312 
7313     public LightPowerModificationUndoableEdit(PlanController controller, UserPreferences preferences,
7314                                               float oldPower, HomeLight light, float newPower) {
7315       super(preferences, PlanController.class, "undoLightPowerModificationName");
7316       this.controller = controller;
7317       this.oldPower = oldPower;
7318       this.light = light;
7319       this.newPower = newPower;
7320     }
7321 
7322     @Override
7323     public void undo() throws CannotUndoException {
7324       super.undo();
7325       this.light.setPower(this.oldPower);
7326       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.light}));
7327     }
7328 
7329     @Override
7330     public void redo() throws CannotRedoException {
7331       super.redo();
7332       this.light.setPower(this.newPower);
7333       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.light}));
7334     }
7335   }
7336 
7337   /**
7338    * Posts an undoable operation about <code>piece</code> name offset change.
7339    */
7340   private void postPieceOfFurnitureNameOffset(final HomePieceOfFurniture piece,
7341                                               final float oldNameXOffset,
7342                                               final float oldNameYOffset) {
7343     final float newNameXOffset = piece.getNameXOffset();
7344     final float newNameYOffset = piece.getNameYOffset();
7345     if (newNameXOffset != oldNameXOffset
7346         || newNameYOffset != oldNameYOffset) {
7347       this.undoSupport.postEdit(new PieceOfFurnitureNameOffsetModificationUndoableEdit(this, this.preferences,
7348           oldNameXOffset, oldNameYOffset, piece, newNameXOffset, newNameYOffset));
7349     }
7350   }
7351 
7352   /**
7353    * Undoable edit for the name offset modification of a piece of furniture.
7354    */
7355   private static class PieceOfFurnitureNameOffsetModificationUndoableEdit extends LocalizedUndoableEdit {
7356     private final PlanController       controller;
7357     private final float                oldNameXOffset;
7358     private final float                oldNameYOffset;
7359     private final HomePieceOfFurniture piece;
7360     private final float                newNameXOffset;
7361     private final float                newNameYOffset;
7362 
7363     public PieceOfFurnitureNameOffsetModificationUndoableEdit(PlanController controller, UserPreferences preferences,
7364                                                               float oldNameXOffset, float oldNameYOffset, HomePieceOfFurniture piece,
7365                                                               float newNameXOffset, float newNameYOffset) {
7366       super(preferences, PlanController.class, "undoPieceOfFurnitureNameOffsetName");
7367       this.controller = controller;
7368       this.oldNameXOffset = oldNameXOffset;
7369       this.oldNameYOffset = oldNameYOffset;
7370       this.piece = piece;
7371       this.newNameXOffset = newNameXOffset;
7372       this.newNameYOffset = newNameYOffset;
7373     }
7374 
7375     @Override
7376     public void undo() throws CannotUndoException {
7377       super.undo();
7378       this.piece.setNameXOffset(this.oldNameXOffset);
7379       this.piece.setNameYOffset(this.oldNameYOffset);
7380       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
7381     }
7382 
7383     @Override
7384     public void redo() throws CannotRedoException {
7385       super.redo();
7386       this.piece.setNameXOffset(this.newNameXOffset);
7387       this.piece.setNameYOffset(this.newNameYOffset);
7388       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
7389     }
7390   }
7391 
7392   /**
7393    * Posts an undoable operation about <code>piece</code> name angle change.
7394    */
7395   private void postPieceOfFurnitureNameRotation(final HomePieceOfFurniture piece,
7396                                                 final float oldNameAngle) {
7397     final float newNameAngle = piece.getNameAngle();
7398     if (newNameAngle != oldNameAngle) {
7399       this.undoSupport.postEdit(new PieceOfFurnitureNameRotationUndoableEdit(this, this.preferences,
7400           oldNameAngle, piece, newNameAngle));
7401     }
7402   }
7403 
7404   /**
7405    * Undoable edit for the name rotation of a piece of furniture.
7406    */
7407   private static class PieceOfFurnitureNameRotationUndoableEdit extends LocalizedUndoableEdit {
7408     private final PlanController       controller;
7409     private final float                oldNameAngle;
7410     private final HomePieceOfFurniture piece;
7411     private final float                newNameAngle;
7412 
7413     public PieceOfFurnitureNameRotationUndoableEdit(PlanController controller, UserPreferences preferences,
7414                                                     float oldNameAngle, HomePieceOfFurniture piece, float newNameAngle) {
7415       super(preferences, PlanController.class, "undoPieceOfFurnitureNameRotationName");
7416       this.controller = controller;
7417       this.oldNameAngle = oldNameAngle;
7418       this.piece = piece;
7419       this.newNameAngle = newNameAngle;
7420     }
7421 
7422     @Override
7423     public void undo() throws CannotUndoException {
7424       super.undo();
7425       this.piece.setNameAngle(this.oldNameAngle);
7426       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
7427     }
7428 
7429     @Override
7430     public void redo() throws CannotRedoException {
7431       super.redo();
7432       this.piece.setNameAngle(this.newNameAngle);
7433       this.controller.selectAndShowItems(Arrays.asList(new HomePieceOfFurniture [] {this.piece}));
7434     }
7435   }
7436 
7437   /**
7438    * Posts an undoable operation about <code>dimensionLine</code> resizing.
7439    */
7440   private void postDimensionLineResize(final DimensionLine dimensionLine, final float oldX, final float oldY,
7441                                        final boolean startPoint, final boolean reversed) {
7442     final float newX;
7443     final float newY;
7444     if (startPoint) {
7445       newX = dimensionLine.getXStart();
7446       newY = dimensionLine.getYStart();
7447     } else {
7448       newX = dimensionLine.getXEnd();
7449       newY = dimensionLine.getYEnd();
7450     }
7451     if (newX != oldX || newY != oldY || reversed) {
7452       this.undoSupport.postEdit(new DimensionLineResizingUndoableEdit(this, this.preferences,
7453           oldX, oldY, dimensionLine, newX, newY, startPoint, reversed));
7454     }
7455   }
7456 
7457   /**
7458    * Undoable edit for dimension line resizing.
7459    */
7460   private static class DimensionLineResizingUndoableEdit extends LocalizedUndoableEdit {
7461     private final PlanController controller;
7462     private final float          oldX;
7463     private final float          oldY;
7464     private final DimensionLine  dimensionLine;
7465     private final float          newX;
7466     private final float          newY;
7467     private final boolean        startPoint;
7468     private final boolean        reversed;
7469 
7470     public DimensionLineResizingUndoableEdit(PlanController controller, UserPreferences preferences,
7471                                              float oldX, float oldY, DimensionLine dimensionLine,
7472                                              float newX, float newY, boolean startPoint, boolean reversed) {
7473       super(preferences, PlanController.class, "undoDimensionLineResizeName");
7474       this.controller = controller;
7475       this.oldX = oldX;
7476       this.oldY = oldY;
7477       this.dimensionLine = dimensionLine;
7478       this.newX = newX;
7479       this.newY = newY;
7480       this.startPoint = startPoint;
7481       this.reversed = reversed;
7482     }
7483 
7484     @Override
7485     public void undo() throws CannotUndoException {
7486       super.undo();
7487       if (this.reversed) {
7488         reverseDimensionLine(this.dimensionLine);
7489         moveDimensionLinePoint(this.dimensionLine, this.oldX, this.oldY, !this.startPoint);
7490       } else {
7491         moveDimensionLinePoint(this.dimensionLine, this.oldX, this.oldY, this.startPoint);
7492       }
7493       this.controller.selectAndShowItems(Arrays.asList(new DimensionLine [] {this.dimensionLine}));
7494     }
7495 
7496     @Override
7497     public void redo() throws CannotRedoException {
7498       super.redo();
7499       if (this.reversed) {
7500         moveDimensionLinePoint(this.dimensionLine, this.newX, this.newY, !this.startPoint);
7501         reverseDimensionLine(this.dimensionLine);
7502       } else {
7503         moveDimensionLinePoint(this.dimensionLine, this.newX, this.newY, this.startPoint);
7504       }
7505       this.controller.selectAndShowItems(Arrays.asList(new DimensionLine [] {this.dimensionLine}));
7506     }
7507   }
7508 
7509   /**
7510    * Posts an undoable operation about <code>dimensionLine</code> offset change.
7511    */
7512   private void postDimensionLineOffset(final DimensionLine dimensionLine, final float oldOffset) {
7513     final float newOffset = dimensionLine.getOffset();
7514     if (newOffset != oldOffset) {
7515       this.undoSupport.postEdit(new DimensionLineOffsetModificationUndoableEdit(this, this.preferences,
7516           oldOffset, dimensionLine, newOffset));
7517     }
7518   }
7519 
7520   /**
7521    * Undoable edit for dimension line offset modification.
7522    */
7523   private static class DimensionLineOffsetModificationUndoableEdit extends LocalizedUndoableEdit {
7524     private final PlanController controller;
7525     private final float          oldOffset;
7526     private final DimensionLine  dimensionLine;
7527     private final float          newOffset;
7528 
7529     public DimensionLineOffsetModificationUndoableEdit(PlanController controller, UserPreferences preferences,
7530                                                        float oldOffset, DimensionLine dimensionLine, float newOffset) {
7531       super(preferences, PlanController.class, "undoDimensionLineOffsetName");
7532       this.controller = controller;
7533       this.oldOffset = oldOffset;
7534       this.dimensionLine = dimensionLine;
7535       this.newOffset = newOffset;
7536     }
7537 
7538     @Override
7539     public void undo() throws CannotUndoException {
7540       super.undo();
7541       this.dimensionLine.setOffset(this.oldOffset);
7542       this.controller.selectAndShowItems(Arrays.asList(new DimensionLine [] {this.dimensionLine}));
7543     }
7544 
7545     @Override
7546     public void redo() throws CannotRedoException {
7547       super.redo();
7548       this.dimensionLine.setOffset(this.newOffset);
7549       this.controller.selectAndShowItems(Arrays.asList(new DimensionLine [] {this.dimensionLine}));
7550     }
7551   }
7552 
7553   /**
7554    * Posts an undoable operation about <code>polyline</code> resizing.
7555    */
7556   private void postPolylineResize(final Polyline polyline, final float oldX, final float oldY,
7557                                   final int pointIndex) {
7558     float [] polylinePoint = polyline.getPoints() [pointIndex];
7559     final float newX = polylinePoint [0];
7560     final float newY = polylinePoint [1];
7561     if (newX != oldX || newY != oldY) {
7562       this.undoSupport.postEdit(new PolylineResizingUndoableEdit(this, this.preferences,
7563           oldX, oldY, polyline, pointIndex, newX, newY));
7564     }
7565   }
7566 
7567   /**
7568    * Undoable edit for polyline resizing.
7569    */
7570   private static class PolylineResizingUndoableEdit extends LocalizedUndoableEdit {
7571     private final PlanController controller;
7572     private final float          oldX;
7573     private final float          oldY;
7574     private final Polyline       polyline;
7575     private final int            pointIndex;
7576     private final float          newX;
7577     private final float          newY;
7578 
7579     public PolylineResizingUndoableEdit(PlanController controller, UserPreferences preferences,
7580                                         float oldX, float oldY, Polyline polyline, int pointIndex, float newX, float newY) {
7581       super(preferences, PlanController.class, "undoPolylineResizeName");
7582       this.controller = controller;
7583       this.oldX = oldX;
7584       this.oldY = oldY;
7585       this.polyline = polyline;
7586       this.pointIndex = pointIndex;
7587       this.newX = newX;
7588       this.newY = newY;
7589     }
7590 
7591     @Override
7592     public void undo() throws CannotUndoException {
7593       super.undo();
7594       this.polyline.setPoint(this.oldX, this.oldY, this.pointIndex);
7595       this.controller.selectAndShowItems(Arrays.asList(new Polyline [] {this.polyline}));
7596     }
7597 
7598     @Override
7599     public void redo() throws CannotRedoException {
7600       super.redo();
7601       this.polyline.setPoint(this.newX, this.newY, this.pointIndex);
7602       this.controller.selectAndShowItems(Arrays.asList(new Polyline [] {this.polyline}));
7603     }
7604   }
7605 
7606   /**
7607    * Post to undo support a north direction change on <code>compass</code>.
7608    */
7609   private void postCompassRotation(final Compass compass,
7610                                    final float oldNorthDirection) {
7611     final float newNorthDirection = compass.getNorthDirection();
7612     if (newNorthDirection != oldNorthDirection) {
7613       this.undoSupport.postEdit(new CompassRotationUndoableEdit(this, this.preferences,
7614           oldNorthDirection, compass, newNorthDirection));
7615     }
7616   }
7617 
7618   /**
7619    * Undoable edit for compass rotation.
7620    */
7621   private static class CompassRotationUndoableEdit extends LocalizedUndoableEdit {
7622     private final PlanController controller;
7623     private final float          oldNorthDirection;
7624     private final Compass        compass;
7625     private final float          newNorthDirection;
7626 
7627     public CompassRotationUndoableEdit(PlanController controller, UserPreferences preferences,
7628                                        float oldNorthDirection, Compass compass, float newNorthDirection) {
7629       super(preferences, PlanController.class, "undoCompassRotationName");
7630       this.controller = controller;
7631       this.compass = compass;
7632       this.newNorthDirection = newNorthDirection;
7633       this.oldNorthDirection = oldNorthDirection;
7634     }
7635 
7636     @Override
7637     public void undo() throws CannotUndoException {
7638       super.undo();
7639       this.compass.setNorthDirection(this.oldNorthDirection);
7640       this.controller.selectAndShowItems(Arrays.asList(new Compass [] {this.compass}));
7641     }
7642 
7643     @Override
7644     public void redo() throws CannotRedoException {
7645       super.redo();
7646       this.compass.setNorthDirection(this.newNorthDirection);
7647       this.controller.selectAndShowItems(Arrays.asList(new Compass [] {this.compass}));
7648     }
7649   }
7650 
7651   /**
7652    * Post to undo support a size change on <code>compass</code>.
7653    */
7654   private void postCompassResize(final Compass compass,
7655                                  final float oldDiameter) {
7656     final float newDiameter = compass.getDiameter();
7657     if (newDiameter != oldDiameter) {
7658       this.undoSupport.postEdit(new CompassResizingUndoableEdit(this, this.preferences,
7659           oldDiameter, compass, newDiameter));
7660     }
7661   }
7662 
7663   /**
7664    * Undoable edit for compass resizing.
7665    */
7666   private static class CompassResizingUndoableEdit extends LocalizedUndoableEdit {
7667     private final PlanController controller;
7668     private final float          oldDiameter;
7669     private final Compass        compass;
7670     private final float          newDiameter;
7671 
7672     public CompassResizingUndoableEdit(PlanController controller, UserPreferences preferences,
7673                                        float oldDiameter, Compass compass, float newDiameter) {
7674       super(preferences, PlanController.class, "undoCompassResizeName");
7675       this.controller = controller;
7676       this.oldDiameter = oldDiameter;
7677       this.compass = compass;
7678       this.newDiameter = newDiameter;
7679     }
7680 
7681     @Override
7682     public void undo() throws CannotUndoException {
7683       super.undo();
7684       this.compass.setDiameter(this.oldDiameter);
7685       this.controller.selectAndShowItems(Arrays.asList(new Compass [] {this.compass}));
7686     }
7687 
7688     @Override
7689     public void redo() throws CannotRedoException {
7690       super.redo();
7691       this.compass.setDiameter(this.newDiameter);
7692       this.controller.selectAndShowItems(Arrays.asList(new Compass [] {this.compass}));
7693     }
7694   }
7695 
7696   /**
7697    * Returns the points of a general path which contains only one path.
7698    */
7699   private float [][] getPathPoints(GeneralPath path,
7700                                    boolean removeAlignedPoints) {
7701     List<float []> pathPoints = new ArrayList<float[]>();
7702     float [] previousPathPoint = null;
7703     for (PathIterator it = path.getPathIterator(null); !it.isDone(); it.next()) {
7704       float [] pathPoint = new float[2];
7705       if (it.currentSegment(pathPoint) != PathIterator.SEG_CLOSE
7706           && (previousPathPoint == null
7707               || !Arrays.equals(pathPoint, previousPathPoint))) {
7708         boolean replacePoint = false;
7709         if (removeAlignedPoints
7710             && pathPoints.size() > 1) {
7711           // Check if pathPoint is aligned with the last line added to pathPoints
7712           float [] lastLineStartPoint = pathPoints.get(pathPoints.size() - 2);
7713           float [] lastLineEndPoint = previousPathPoint;
7714           replacePoint = Line2D.ptLineDistSq(lastLineStartPoint [0], lastLineStartPoint [1],
7715               lastLineEndPoint [0], lastLineEndPoint [1],
7716               pathPoint [0], pathPoint [1]) < 0.0001;
7717         }
7718         if (replacePoint) {
7719           pathPoints.set(pathPoints.size() - 1, pathPoint);
7720         } else {
7721           pathPoints.add(pathPoint);
7722         }
7723         previousPathPoint = pathPoint;
7724       }
7725     }
7726 
7727     // Remove last point if it's equal to first point
7728     if (pathPoints.size() > 1
7729         && Arrays.equals(pathPoints.get(0), pathPoints.get(pathPoints.size() - 1))) {
7730       pathPoints.remove(pathPoints.size() - 1);
7731     }
7732 
7733     return pathPoints.toArray(new float [pathPoints.size()][]);
7734   }
7735 
7736   /**
7737    * Returns the list of closed paths that may define rooms from
7738    * the current set of home walls.
7739    */
7740   private List<GeneralPath> getRoomPathsFromWalls() {
7741     if (this.roomPathsCache == null) {
7742       // Iterate over all the paths the walls area contains
7743       Area wallsArea = getWallsArea(false);
7744       List<GeneralPath> roomPaths = getAreaPaths(wallsArea);
7745       Area insideWallsArea = new Area(wallsArea);
7746       for (GeneralPath roomPath : roomPaths) {
7747         insideWallsArea.add(new Area(roomPath));
7748       }
7749 
7750       this.roomPathsCache = roomPaths;
7751       this.insideWallsAreaCache = insideWallsArea;
7752     }
7753     return this.roomPathsCache;
7754   }
7755 
7756   /**
7757    * Returns the paths described by the given <code>area</code>.
7758    */
7759   private List<GeneralPath> getAreaPaths(Area area) {
7760     List<GeneralPath> roomPaths = new ArrayList<GeneralPath>();
7761     GeneralPath roomPath = null;
7762     float [] previousRoomPoint = null;
7763     for (PathIterator it = area.getPathIterator(null, 0.5f); !it.isDone(); it.next()) {
7764       float [] roomPoint = new float[2];
7765       switch (it.currentSegment(roomPoint)) {
7766         case PathIterator.SEG_MOVETO :
7767           roomPath = new GeneralPath();
7768           roomPath.moveTo(roomPoint [0], roomPoint [1]);
7769           previousRoomPoint = roomPoint;
7770           break;
7771         case PathIterator.SEG_LINETO :
7772           if ((roomPoint [0] != previousRoomPoint [0]
7773                 || roomPoint [1] != previousRoomPoint [1])
7774               && Point2D.distanceSq(roomPoint [0], roomPoint [1], previousRoomPoint [0], previousRoomPoint [1]) > 1E-10) {
7775             roomPath.lineTo(roomPoint [0], roomPoint [1]);
7776             previousRoomPoint = roomPoint;
7777           }
7778           break;
7779         case PathIterator.SEG_CLOSE :
7780           roomPath.closePath();
7781           roomPaths.add(roomPath);
7782           break;
7783       }
7784     }
7785     return roomPaths;
7786   }
7787 
7788   /**
7789    * Returns the area that includes walls and inside walls area.
7790    */
7791   private Area getInsideWallsArea() {
7792     if (this.insideWallsAreaCache == null) {
7793       getRoomPathsFromWalls();
7794     }
7795     return this.insideWallsAreaCache;
7796   }
7797 
7798   /**
7799    * Returns the area covered by walls.
7800    */
7801   private Area getWallsArea(boolean includeBaseboards) {
7802     if (!includeBaseboards && this.wallsAreaCache == null
7803         || includeBaseboards && this.wallsIncludingBaseboardsAreaCache == null) {
7804       // Compute walls area
7805       Area wallsArea = new Area();
7806       Level selectedLevel = this.home.getSelectedLevel();
7807       for (Wall wall : this.home.getWalls()) {
7808         if (wall.isAtLevel(selectedLevel)) {
7809           wallsArea.add(new Area(getPath(wall.getPoints(includeBaseboards))));
7810         }
7811       }
7812       if (includeBaseboards) {
7813         this.wallsIncludingBaseboardsAreaCache = wallsArea;
7814       } else {
7815         this.wallsAreaCache = wallsArea;
7816       }
7817     }
7818     return includeBaseboards
7819         ? this.wallsIncludingBaseboardsAreaCache
7820         : this.wallsAreaCache;
7821   }
7822 
7823   /**
7824    * Returns the shape matching the coordinates in <code>points</code> array.
7825    */
7826   private GeneralPath getPath(float [][] points) {
7827     GeneralPath path = new GeneralPath();
7828     path.moveTo(points [0][0], points [0][1]);
7829     for (int i = 1; i < points.length; i++) {
7830       path.lineTo(points [i][0], points [i][1]);
7831     }
7832     path.closePath();
7833     return path;
7834   }
7835 
7836   /**
7837    * Returns the path matching a given area.
7838    */
7839   private GeneralPath getPath(Area area) {
7840     GeneralPath path = new GeneralPath();
7841     float [] point = new float [2];
7842     for (PathIterator it = area.getPathIterator(null, 0.5f); !it.isDone(); it.next()) {
7843       switch (it.currentSegment(point)) {
7844         case PathIterator.SEG_MOVETO :
7845           path.moveTo(point [0], point [1]);
7846           break;
7847         case PathIterator.SEG_LINETO :
7848           path.lineTo(point [0], point [1]);
7849           break;
7850       }
7851     }
7852     return path;
7853   }
7854 
7855   /**
7856    * Selects the level of the first elevatable item in the current selection
7857    * if no selected item is visible at the selected level.
7858    */
7859   private void selectLevelFromSelectedItems() {
7860     Level selectedLevel = this.home.getSelectedLevel();
7861     List<Selectable> selectedItems = this.home.getSelectedItems();
7862     for (Object item : selectedItems) {
7863       if (item instanceof Elevatable
7864           && ((Elevatable)item).isAtLevel(selectedLevel)) {
7865         return;
7866       }
7867     }
7868 
7869     for (Object item : selectedItems) {
7870       if (item instanceof Elevatable) {
7871         setSelectedLevel(((Elevatable)item).getLevel());
7872         break;
7873       }
7874     }
7875   }
7876 
7877   /**
7878    * Stores the size of a resized piece of furniture.
7879    */
7880   private static class ResizedPieceOfFurniture {
7881     private final HomePieceOfFurniture piece;
7882     private final float                x;
7883     private final float                y;
7884     private final float                width;
7885     private final float                depth;
7886     private final float                height;
7887     private final boolean              doorOrWindowBoundToWall;
7888     private final float []             groupFurnitureX;
7889     private final float []             groupFurnitureY;
7890     private final float []             groupFurnitureWidth;
7891     private final float []             groupFurnitureDepth;
7892     private final float []             groupFurnitureHeight;
7893 
7894     public ResizedPieceOfFurniture(HomePieceOfFurniture piece) {
7895       this.piece = piece;
7896       this.x = piece.getX();
7897       this.y = piece.getY();
7898       this.width = piece.getWidth();
7899       this.depth = piece.getDepth();
7900       this.height = piece.getHeight();
7901       this.doorOrWindowBoundToWall = piece instanceof HomeDoorOrWindow
7902           && ((HomeDoorOrWindow)piece).isBoundToWall();
7903       if (piece instanceof HomeFurnitureGroup) {
7904         List<HomePieceOfFurniture> groupFurniture = ((HomeFurnitureGroup)piece).getAllFurniture();
7905         this.groupFurnitureX = new float [groupFurniture.size()];
7906         this.groupFurnitureY = new float [groupFurniture.size()];
7907         this.groupFurnitureWidth = new float [groupFurniture.size()];
7908         this.groupFurnitureDepth = new float [groupFurniture.size()];
7909         this.groupFurnitureHeight = new float [groupFurniture.size()];
7910         for (int i = 0; i < groupFurniture.size(); i++) {
7911           HomePieceOfFurniture groupPiece = groupFurniture.get(i);
7912           this.groupFurnitureX [i] = groupPiece.getX();
7913           this.groupFurnitureY [i] = groupPiece.getY();
7914           this.groupFurnitureWidth [i] = groupPiece.getWidth();
7915           this.groupFurnitureDepth [i] = groupPiece.getDepth();
7916           this.groupFurnitureHeight [i] = groupPiece.getHeight();
7917         }
7918       } else {
7919         this.groupFurnitureX = null;
7920         this.groupFurnitureY = null;
7921         this.groupFurnitureWidth = null;
7922         this.groupFurnitureDepth = null;
7923         this.groupFurnitureHeight = null;
7924       }
7925     }
7926 
7927     public HomePieceOfFurniture getPieceOfFurniture() {
7928       return this.piece;
7929     }
7930 
7931     public float getWidth() {
7932       return this.width;
7933     }
7934 
7935     public float getDepth() {
7936       return this.depth;
7937     }
7938 
7939     public float getHeight() {
7940       return this.height;
7941     }
7942 
7943     public boolean isDoorOrWindowBoundToWall() {
7944       return this.doorOrWindowBoundToWall;
7945     }
7946 
7947     public void reset() {
7948       this.piece.setX(this.x);
7949       this.piece.setY(this.y);
7950       setPieceOfFurnitureSize(this.piece, this.width, this.depth, this.height);
7951       if (this.piece instanceof HomeDoorOrWindow) {
7952         ((HomeDoorOrWindow)this.piece).setBoundToWall(this.doorOrWindowBoundToWall);
7953       }
7954       if (this.piece instanceof HomeFurnitureGroup) {
7955         List<HomePieceOfFurniture> groupFurniture = ((HomeFurnitureGroup)this.piece).getAllFurniture();
7956         for (int i = 0; i < groupFurniture.size(); i++) {
7957           HomePieceOfFurniture groupPiece = groupFurniture.get(i);
7958           if (this.piece.isResizable()) {
7959             // Restore group furniture location and size because resizing a group isn't reversible
7960             groupPiece.setX(this.groupFurnitureX [i]);
7961             groupPiece.setY(this.groupFurnitureY [i]);
7962             setPieceOfFurnitureSize(groupPiece,
7963                 this.groupFurnitureWidth [i], this.groupFurnitureDepth [i], this.groupFurnitureHeight [i]);
7964           }
7965         }
7966       }
7967     }
7968 
7969     public static void setPieceOfFurnitureSize(HomePieceOfFurniture piece,
7970                                                float width, float depth, float height) {
7971       if (piece.isHorizontallyRotated()) {
7972         // Furniture rotated around horizontal axes are always changed proportionally
7973         float scale = width / piece.getWidth();
7974         piece.scale(scale);
7975         piece.setWidthInPlan(scale * piece.getWidthInPlan());
7976         piece.setDepthInPlan(scale * piece.getDepthInPlan());
7977         piece.setHeightInPlan(scale * piece.getHeightInPlan());
7978         if (piece instanceof HomeFurnitureGroup) {
7979           for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
7980             childPiece.setWidthInPlan(scale * childPiece.getWidthInPlan());
7981             childPiece.setDepthInPlan(scale * childPiece.getDepthInPlan());
7982             childPiece.setHeightInPlan(scale * childPiece.getHeightInPlan());
7983           }
7984         }
7985       } else {
7986         float widthInPlan = piece.getWidthInPlan() * width / piece.getWidth();
7987         piece.setWidth(width);
7988         piece.setWidthInPlan(widthInPlan);
7989         float depthInPlan = piece.getDepthInPlan() * depth / piece.getDepth();
7990         piece.setDepth(depth);
7991         piece.setDepthInPlan(depthInPlan);
7992         float heightInPlan = piece.getHeightInPlan() * height / piece.getHeight();
7993         piece.setHeight(height);
7994         piece.setHeightInPlan(heightInPlan);
7995         if (piece instanceof HomeFurnitureGroup) {
7996           for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
7997             childPiece.setWidthInPlan(childPiece.getWidth());
7998             childPiece.setDepthInPlan(childPiece.getDepth());
7999             childPiece.setHeightInPlan(childPiece.getHeight());
8000           }
8001         }
8002       }
8003     }
8004   }
8005 
8006   /**
8007    * Stores the walls at start and at end of a given wall. This data are useful
8008    * to add a collection of walls after an undo/redo delete operation.
8009    */
8010   private static final class JoinedWall {
8011     private final Wall    wall;
8012     private final Level   level;
8013     private final float   xStart;
8014     private final float   yStart;
8015     private final float   xEnd;
8016     private final float   yEnd;
8017     private final Wall    wallAtStart;
8018     private final Wall    wallAtEnd;
8019     private final boolean joinedAtEndOfWallAtStart;
8020     private final boolean joinedAtStartOfWallAtEnd;
8021 
8022     public JoinedWall(Wall wall) {
8023       this.wall = wall;
8024       this.level = wall.getLevel();
8025       this.xStart = wall.getXStart();
8026       this.xEnd = wall.getXEnd();
8027       this.yStart = wall.getYStart();
8028       this.yEnd = wall.getYEnd();
8029       this.wallAtStart = wall.getWallAtStart();
8030       this.joinedAtEndOfWallAtStart =
8031           this.wallAtStart != null
8032           && this.wallAtStart.getWallAtEnd() == wall;
8033       this.wallAtEnd = wall.getWallAtEnd();
8034       this.joinedAtStartOfWallAtEnd =
8035           this.wallAtEnd != null
8036           && wallAtEnd.getWallAtStart() == wall;
8037     }
8038 
8039     public Wall getWall() {
8040       return this.wall;
8041     }
8042 
8043     public Level getLevel() {
8044       return this.level;
8045     }
8046 
8047     public float getXStart() {
8048       return this.xStart;
8049     }
8050 
8051     public float getYStart() {
8052       return this.yStart;
8053     }
8054 
8055     public float getXEnd() {
8056       return this.xEnd;
8057     }
8058 
8059     public float getYEnd() {
8060       return this.yEnd;
8061     }
8062 
8063     public Wall getWallAtEnd() {
8064       return this.wallAtEnd;
8065     }
8066 
8067     public Wall getWallAtStart() {
8068       return this.wallAtStart;
8069     }
8070 
8071     public boolean isJoinedAtEndOfWallAtStart() {
8072       return this.joinedAtEndOfWallAtStart;
8073     }
8074 
8075     public boolean isJoinedAtStartOfWallAtEnd() {
8076       return this.joinedAtStartOfWallAtEnd;
8077     }
8078 
8079     /**
8080      * A helper method that builds an array of <code>JoinedWall</code> objects
8081      * for a given list of walls.
8082      */
8083     public static JoinedWall [] getJoinedWalls(List<Wall> walls) {
8084       JoinedWall [] joinedWalls = new JoinedWall [walls.size()];
8085       for (int i = 0; i < joinedWalls.length; i++) {
8086         joinedWalls [i] = new JoinedWall(walls.get(i));
8087       }
8088       return joinedWalls;
8089     }
8090 
8091     /**
8092      * A helper method that builds a list of <code>Wall</code> objects
8093      * for a given array of <code>JoinedWall</code> objects.
8094      */
8095     public static List<Wall> getWalls(JoinedWall [] joinedWalls) {
8096       Wall [] walls = new Wall [joinedWalls.length];
8097       for (int i = 0; i < joinedWalls.length; i++) {
8098         walls [i] = joinedWalls [i].getWall();
8099       }
8100       return Arrays.asList(walls);
8101     }
8102   }
8103 
8104   /**
8105    * A point which coordinates are computed with an angle magnetism algorithm.
8106    */
8107   private static class PointWithAngleMagnetism {
8108     private static final int CIRCLE_STEPS_15_DEG = 24;
8109 
8110     private float x;
8111     private float y;
8112     private float angle;
8113 
8114     /**
8115      * Create a point that applies angle magnetism to point (<code>x</code>, <code>y</code>).
8116      * Point end coordinates stored at object initialization may be different from x or y,
8117      * to match the closest point belonging to one of the radius of a circle centered at
8118      * (<code>xStart</code>, <code>yStart</code>), each radius being a multiple of 15 degrees.
8119      * The length of the line joining (<code>xStart</code>, <code>yStart</code>) to the computed
8120      * point is approximated depending on the current <code>unit</code> and scale.
8121      */
8122     public PointWithAngleMagnetism(float xStart, float yStart, float x, float y,
8123                                    LengthUnit unit, float maxLengthDelta) {
8124       this(xStart, yStart, x, y, unit, maxLengthDelta, CIRCLE_STEPS_15_DEG);
8125     }
8126 
8127     public PointWithAngleMagnetism(float xStart, float yStart, float x, float y,
8128                                    LengthUnit unit, float maxLengthDelta, int circleSteps) {
8129       this.x = x;
8130       this.y = y;
8131       if (xStart == x) {
8132         // Apply magnetism to the length of the line joining start point to magnetized point
8133         float magnetizedLength = unit.getMagnetizedLength(Math.abs(yStart - y), maxLengthDelta);
8134         this.y = yStart + (float)(magnetizedLength * Math.signum(y - yStart));
8135       } else if (yStart == y) {
8136         // Apply magnetism to the length of the line joining start point to magnetized point
8137         float magnetizedLength = unit.getMagnetizedLength(Math.abs(xStart - x), maxLengthDelta);
8138         this.x = xStart + (float)(magnetizedLength * Math.signum(x - xStart));
8139       } else { // xStart != x && yStart != y
8140         double angleStep = 2 * Math.PI / circleSteps;
8141         // Caution : pixel coordinate space is indirect !
8142         double angle = Math.atan2(yStart - y, x - xStart);
8143         // Compute previous angle closest to a step angle (multiple of angleStep)
8144         double previousStepAngle = Math.floor(angle / angleStep) * angleStep;
8145         double angle1;
8146         double tanAngle1;
8147         double angle2;
8148         double tanAngle2;
8149         // Compute the tan of previousStepAngle and the next step angle
8150         if (Math.tan(angle) > 0) {
8151           angle1 = previousStepAngle;
8152           tanAngle1 = Math.tan(previousStepAngle);
8153           angle2 = previousStepAngle + angleStep;
8154           tanAngle2 = Math.tan(previousStepAngle + angleStep);
8155         } else {
8156           // If slope is negative inverse the order of the two angles
8157           angle1 = previousStepAngle + angleStep;
8158           tanAngle1 = Math.tan(previousStepAngle + angleStep);
8159           angle2 = previousStepAngle;
8160           tanAngle2 = Math.tan(previousStepAngle);
8161         }
8162         // Search in the first quarter of the trigonometric circle,
8163         // the point (xEnd1,yEnd1) or (xEnd2,yEnd2) closest to point
8164         // (xEnd,yEnd) that belongs to angle 1 or angle 2 radius
8165         double firstQuarterTanAngle1 = Math.abs(tanAngle1);
8166         double firstQuarterTanAngle2 = Math.abs(tanAngle2);
8167         float xEnd1 = Math.abs(xStart - x);
8168         float yEnd2 = Math.abs(yStart - y);
8169         float xEnd2 = 0;
8170         // If angle 2 is greater than 0 rad
8171         if (firstQuarterTanAngle2 > 1E-10) {
8172           // Compute the abscissa of point 2 that belongs to angle 1 radius at
8173           // y2 ordinate
8174           xEnd2 = (float)(yEnd2 / firstQuarterTanAngle2);
8175         }
8176         float yEnd1 = 0;
8177         // If angle 1 is smaller than PI / 2 rad
8178         if (firstQuarterTanAngle1 < 1E10) {
8179           // Compute the ordinate of point 1 that belongs to angle 1 radius at
8180           // x1 abscissa
8181           yEnd1 = (float)(xEnd1 * firstQuarterTanAngle1);
8182         }
8183 
8184         // Apply magnetism to the smallest distance
8185         double magnetismAngle;
8186         if (Math.abs(xEnd2 - xEnd1) < Math.abs(yEnd1 - yEnd2)) {
8187           magnetismAngle = angle2;
8188           this.x = xStart + (float)((yStart - y) / tanAngle2);
8189         } else {
8190           magnetismAngle = angle1;
8191           this.y = yStart - (float)((x - xStart) * tanAngle1);
8192         }
8193 
8194         // Apply magnetism to the length of the line joining start point
8195         // to magnetized point
8196         float magnetizedLength = unit.getMagnetizedLength((float)Point2D.distance(xStart, yStart,
8197             this.x, this.y), maxLengthDelta);
8198         this.x = xStart + (float)(magnetizedLength * Math.cos(magnetismAngle));
8199         this.y = yStart - (float)(magnetizedLength * Math.sin(magnetismAngle));
8200         this.angle = (float)magnetismAngle;
8201       }
8202     }
8203 
8204     /**
8205      * Returns the abscissa of end point.
8206      */
8207     public float getX() {
8208       return this.x;
8209     }
8210 
8211     /**
8212      * Sets the abscissa of end point.
8213      */
8214     protected void setX(float x) {
8215       this.x = x;
8216     }
8217 
8218     /**
8219      * Returns the ordinate of end point.
8220      */
8221     public float getY() {
8222       return this.y;
8223     }
8224 
8225     /**
8226      * Sets the ordinate of end point.
8227      */
8228     protected void setY(float y) {
8229       this.y = y;
8230     }
8231 
8232     protected float getAngle() {
8233       return this.angle;
8234     }
8235   }
8236 
8237   /**
8238    * A point with coordinates computed with angle and wall points magnetism.
8239    */
8240   private class WallPointWithAngleMagnetism extends PointWithAngleMagnetism {
8241     public WallPointWithAngleMagnetism(Wall editedWall, float xWall, float yWall, float x, float y) {
8242       super(xWall, yWall, x, y, preferences.getLengthUnit(), getView().getPixelLength());
8243       float margin = PIXEL_MARGIN / getScale();
8244       // Search which wall start or end point is close to (x, y)
8245       // ignoring the start and end point of editedWall
8246       float deltaXToClosestWall = Float.POSITIVE_INFINITY;
8247       float deltaYToClosestWall = Float.POSITIVE_INFINITY;
8248       float xClosestWall = 0;
8249       float yClosestWall = 0;
8250       for (Wall wall : getDetectableWallsAtSelectedLevel()) {
8251         if (wall != editedWall) {
8252           if (Math.abs(getX() - wall.getXStart()) < margin
8253               && (editedWall == null
8254                   || !equalsWallPoint(wall.getXStart(), wall.getYStart(), editedWall))) {
8255             if (Math.abs(deltaYToClosestWall) > Math.abs(getY() - wall.getYStart())) {
8256               xClosestWall = wall.getXStart();
8257               deltaYToClosestWall = getY() - yClosestWall;
8258             }
8259           } else if (Math.abs(getX() - wall.getXEnd()) < margin
8260                     && (editedWall == null
8261                         || !equalsWallPoint(wall.getXEnd(), wall.getYEnd(), editedWall))) {
8262             if (Math.abs(deltaYToClosestWall) > Math.abs(getY() - wall.getYEnd())) {
8263               xClosestWall = wall.getXEnd();
8264               deltaYToClosestWall = getY() - yClosestWall;
8265             }
8266           }
8267 
8268           if (Math.abs(getY() - wall.getYStart()) < margin
8269               && (editedWall == null
8270                   || !equalsWallPoint(wall.getXStart(), wall.getYStart(), editedWall))) {
8271             if (Math.abs(deltaXToClosestWall) > Math.abs(getX() - wall.getXStart())) {
8272               yClosestWall = wall.getYStart();
8273               deltaXToClosestWall = getX() - xClosestWall;
8274             }
8275           } else if (Math.abs(getY() - wall.getYEnd()) < margin
8276                     && (editedWall == null
8277                         || !equalsWallPoint(wall.getXEnd(), wall.getYEnd(), editedWall))) {
8278             if (Math.abs(deltaXToClosestWall) > Math.abs(getX() - wall.getXEnd())) {
8279               yClosestWall = wall.getYEnd();
8280               deltaXToClosestWall = getX() - xClosestWall;
8281             }
8282           }
8283         }
8284       }
8285 
8286       if (editedWall != null) {
8287         double alpha = -Math.tan(getAngle());
8288         double beta = Math.abs(alpha) < 1E10
8289             ? yWall - alpha * xWall
8290             : Double.POSITIVE_INFINITY;
8291         if (deltaXToClosestWall != Float.POSITIVE_INFINITY && Math.abs(alpha) > 1E-10) {
8292           float newX = (float)((yClosestWall - beta) / alpha);
8293           if (Point2D.distanceSq(getX(), getY(), newX, yClosestWall) <= margin * margin) {
8294             setX(newX);
8295             setY(yClosestWall);
8296             return;
8297           }
8298         }
8299         if (deltaYToClosestWall != Float.POSITIVE_INFINITY
8300             && beta != Double.POSITIVE_INFINITY) {
8301           float newY = (float)(alpha * xClosestWall + beta);
8302           if (Point2D.distanceSq(getX(), getY(), xClosestWall, newY) <= margin * margin) {
8303             setX(xClosestWall);
8304             setY(newY);
8305           }
8306         }
8307       } else {
8308         if (deltaXToClosestWall != Float.POSITIVE_INFINITY) {
8309           setY(yClosestWall);
8310         }
8311         if (deltaYToClosestWall != Float.POSITIVE_INFINITY) {
8312           setX(xClosestWall);
8313         }
8314       }
8315     }
8316 
8317     /**
8318      * Returns <code>true</code> if <code>wall</code> start or end point
8319      * equals the point (<code>x</code>, <code>y</code>).
8320      */
8321     private boolean equalsWallPoint(float x, float y, Wall wall) {
8322       return x == wall.getXStart() && y == wall.getYStart()
8323              || x == wall.getXEnd() && y == wall.getYEnd();
8324     }
8325   }
8326 
8327   /**
8328    * A point with coordinates computed with angle and room points magnetism.
8329    */
8330   private class RoomPointWithAngleMagnetism extends PointWithAngleMagnetism {
8331     public RoomPointWithAngleMagnetism(Room editedRoom, int editedPointIndex, float xRoom, float yRoom, float x, float y) {
8332       super(xRoom, yRoom, x, y, preferences.getLengthUnit(), getView().getPixelLength());
8333       float planScale = getScale();
8334       float margin = PIXEL_MARGIN / planScale;
8335       // Search which room points are close to (x, y)
8336       // ignoring the start and end point of alignedRoom
8337       float deltaXToClosestObject = Float.POSITIVE_INFINITY;
8338       float deltaYToClosestObject = Float.POSITIVE_INFINITY;
8339       float xClosestObject = 0;
8340       float yClosestObject = 0;
8341       for (Room room : getDetectableRoomsAtSelectedLevel()) {
8342         float [][] roomPoints = room.getPoints();
8343         for (int i = 0; i < roomPoints.length; i++) {
8344           if (editedPointIndex == -1 || (i != editedPointIndex && roomPoints.length > 2)) {
8345             if (Math.abs(getX() - roomPoints [i][0]) < margin
8346                 && Math.abs(deltaYToClosestObject) > Math.abs(getY() - roomPoints [i][1])) {
8347               xClosestObject = roomPoints [i][0];
8348               deltaYToClosestObject = getY() - roomPoints [i][1];
8349             }
8350             if (Math.abs(getY() - roomPoints [i][1]) < margin
8351                 && Math.abs(deltaXToClosestObject) > Math.abs(getX() - roomPoints [i][0])) {
8352               yClosestObject = roomPoints [i][1];
8353               deltaXToClosestObject = getX() - roomPoints [i][0];
8354             }
8355           }
8356         }
8357       }
8358       // Search which wall points are close to (x, y)
8359       for (Wall wall : getDetectableWallsAtSelectedLevel()) {
8360         float [][] wallPoints = wall.getPoints();
8361         // Take into account only points at start and end of the wall
8362         wallPoints = new float [][] {wallPoints [0], wallPoints [wallPoints.length / 2 - 1],
8363                                      wallPoints [wallPoints.length / 2], wallPoints [wallPoints.length - 1]};
8364         for (int i = 0; i < wallPoints.length; i++) {
8365           if (Math.abs(getX() - wallPoints [i][0]) < margin
8366               && Math.abs(deltaYToClosestObject) > Math.abs(getY() - wallPoints [i][1])) {
8367             xClosestObject = wallPoints [i][0];
8368             deltaYToClosestObject = getY() - wallPoints [i][1];
8369           }
8370           if (Math.abs(getY() - wallPoints [i][1]) < margin
8371               && Math.abs(deltaXToClosestObject) > Math.abs(getX() - wallPoints [i][0])) {
8372             yClosestObject = wallPoints [i][1];
8373             deltaXToClosestObject = getX() - wallPoints [i][0];
8374           }
8375         }
8376       }
8377 
8378       if (editedRoom != null) {
8379         double alpha = -Math.tan(getAngle());
8380         double beta = Math.abs(alpha) < 1E10
8381             ? yRoom - alpha * xRoom
8382             : Double.POSITIVE_INFINITY;
8383         if (deltaXToClosestObject != Float.POSITIVE_INFINITY && Math.abs(alpha) > 1E-10) {
8384           float newX = (float)((yClosestObject - beta) / alpha);
8385           if (Point2D.distanceSq(getX(), getY(), newX, yClosestObject) <= margin * margin) {
8386             setX(newX);
8387             setY(yClosestObject);
8388             return;
8389           }
8390         }
8391         if (deltaYToClosestObject != Float.POSITIVE_INFINITY
8392             && beta != Double.POSITIVE_INFINITY) {
8393           float newY = (float)(alpha * xClosestObject + beta);
8394           if (Point2D.distanceSq(getX(), getY(), xClosestObject, newY) <= margin * margin) {
8395             setX(xClosestObject);
8396             setY(newY);
8397           }
8398         }
8399       } else {
8400         if (deltaXToClosestObject != Float.POSITIVE_INFINITY) {
8401           setY(yClosestObject);
8402         }
8403         if (deltaYToClosestObject != Float.POSITIVE_INFINITY) {
8404           setX(xClosestObject);
8405         }
8406       }
8407     }
8408   }
8409 
8410   /**
8411    * A point which coordinates are equal to the closest point of a wall or a room.
8412    */
8413   private class PointMagnetizedToClosestWallOrRoomPoint {
8414     private float   x;
8415     private float   y;
8416     private boolean magnetized;
8417 
8418     /**
8419      * Creates a point that applies magnetism to point (<code>x</code>, <code>y</code>).
8420      * If this point is close to a point of a wall corner or of a room, it will be initialiazed to its coordinates.
8421      */
8422     public PointMagnetizedToClosestWallOrRoomPoint(float x, float y) {
8423       this(null, -1, x, y);
8424     }
8425 
8426     public PointMagnetizedToClosestWallOrRoomPoint(Room editedRoom, int editedPointIndex, float x, float y) {
8427       float margin = getSelectionMargin();
8428       // Find the closest wall point to (x,y)
8429       double smallestDistance = Double.MAX_VALUE;
8430       for (GeneralPath roomPath : getRoomPathsFromWalls()) {
8431         smallestDistance = updateMagnetizedPoint(-1, x, y,
8432             smallestDistance, getPathPoints(roomPath, false));
8433       }
8434       for (Room room : getDetectableRoomsAtSelectedLevel()) {
8435         smallestDistance = updateMagnetizedPoint(room == editedRoom ? editedPointIndex : - 1,
8436             x, y, smallestDistance, room.getPoints());
8437       }
8438       this.magnetized = smallestDistance <= margin * margin;
8439       if (!this.magnetized) {
8440         // Don't magnetism if closest wall point is too far
8441         this.x = x;
8442         this.y = y;
8443       }
8444     }
8445 
8446     private double updateMagnetizedPoint(int editedPointIndex,
8447                                          float x, float y,
8448                                          double smallestDistance,
8449                                          float [][] points) {
8450       for (int i = 0; i < points.length; i++) {
8451         if (i != editedPointIndex) {
8452           double distance = Point2D.distanceSq(points [i][0], points [i][1], x, y);
8453           if (distance < smallestDistance) {
8454             this.x = points [i][0];
8455             this.y = points [i][1];
8456             smallestDistance = distance;
8457           }
8458         }
8459       }
8460       return smallestDistance;
8461     }
8462 
8463     /**
8464      * Returns the abscissa of end point computed with magnetism.
8465      */
8466     public float getX() {
8467       return this.x;
8468     }
8469 
8470     /**
8471      * Returns the ordinate of end point computed with magnetism.
8472      */
8473     public float getY() {
8474       return this.y;
8475     }
8476 
8477     public boolean isMagnetized() {
8478       return this.magnetized;
8479     }
8480   }
8481 
8482   /**
8483    * Controller state classes super class.
8484    */
8485   protected static abstract class ControllerState {
8486     public void enter() {
8487     }
8488 
8489     public void exit() {
8490     }
8491 
8492     public abstract Mode getMode();
8493 
8494     public void setMode(Mode mode) {
8495     }
8496 
8497     public boolean isModificationState() {
8498       return false;
8499     }
8500 
8501     public boolean isBasePlanModificationState() {
8502       return false;
8503     }
8504 
8505     public void deleteSelection() {
8506     }
8507 
8508     public void escape() {
8509     }
8510 
8511     public void moveSelection(float dx, float dy) {
8512     }
8513 
8514     public void toggleMagnetism(boolean magnetismToggled) {
8515     }
8516 
8517     public void setAlignmentActivated(boolean alignmentActivated) {
8518     }
8519 
8520     public void setDuplicationActivated(boolean duplicationActivated) {
8521     }
8522 
8523     public void setEditionActivated(boolean editionActivated) {
8524     }
8525 
8526     public void updateEditableProperty(EditableProperty editableField, Object value) {
8527     }
8528 
8529     public void pressMouse(float x, float y, int clickCount,
8530                            boolean shiftDown, boolean duplicationActivated) {
8531     }
8532 
8533     public void releaseMouse(float x, float y) {
8534     }
8535 
8536     public void moveMouse(float x, float y) {
8537     }
8538 
8539     public void zoom(float factor) {
8540     }
8541   }
8542 
8543   /**
8544    * A decorator on controller state, useful to change the behavior of an existing state.
8545    * @since 6.4
8546    */
8547   protected static abstract class ControllerStateDecorator extends ControllerState {
8548     private final ControllerState state;
8549 
8550     public ControllerStateDecorator(ControllerState state) {
8551       this.state = state;
8552     }
8553 
8554     @Override
8555     public void enter() {
8556       this.state.enter();
8557     }
8558 
8559     @Override
8560     public void exit() {
8561       this.state.exit();
8562     }
8563 
8564     @Override
8565     public Mode getMode() {
8566       return this.state.getMode();
8567     }
8568 
8569     @Override
8570     public void setMode(Mode mode) {
8571       this.state.setMode(mode);
8572     }
8573 
8574     @Override
8575     public boolean isModificationState() {
8576       return this.state.isModificationState();
8577     }
8578 
8579     @Override
8580     public boolean isBasePlanModificationState() {
8581       return this.state.isBasePlanModificationState();
8582     }
8583 
8584     @Override
8585     public void deleteSelection() {
8586       this.state.deleteSelection();
8587     }
8588 
8589     @Override
8590     public void escape() {
8591       this.state.escape();
8592     }
8593 
8594     @Override
8595     public void moveSelection(float dx, float dy) {
8596       this.state.moveSelection(dx, dy);
8597     }
8598 
8599     @Override
8600     public void toggleMagnetism(boolean magnetismToggled) {
8601       this.state.toggleMagnetism(magnetismToggled);
8602     }
8603 
8604     @Override
8605     public void setAlignmentActivated(boolean alignmentActivated) {
8606       this.state.setAlignmentActivated(alignmentActivated);
8607     }
8608 
8609     @Override
8610     public void setDuplicationActivated(boolean duplicationActivated) {
8611       this.state.setDuplicationActivated(duplicationActivated);
8612     }
8613 
8614     @Override
8615     public void setEditionActivated(boolean editionActivated) {
8616       this.state.setEditionActivated(editionActivated);
8617     }
8618 
8619     @Override
8620     public void updateEditableProperty(EditableProperty editableField,
8621                                        Object value) {
8622       this.state.updateEditableProperty(editableField, value);
8623     }
8624 
8625     @Override
8626     public void pressMouse(float x, float y, int clickCount,
8627                            boolean shiftDown, boolean duplicationActivated) {
8628       this.state.pressMouse(x, y, clickCount, shiftDown, duplicationActivated);
8629     }
8630 
8631     @Override
8632     public void releaseMouse(float x, float y) {
8633       this.state.releaseMouse(x, y);
8634     }
8635 
8636     @Override
8637     public void moveMouse(float x, float y) {
8638       this.state.moveMouse(x, y);
8639     }
8640 
8641     @Override
8642     public void zoom(float factor) {
8643       this.state.zoom(factor);
8644     }
8645   }
8646 
8647   // ControllerState subclasses
8648 
8649   /**
8650    * Abstract state able to manage the transition to other modes.
8651    */
8652   private abstract class AbstractModeChangeState extends ControllerState {
8653     @Override
8654     public void setMode(Mode mode) {
8655       if (mode == Mode.SELECTION) {
8656         setState(getSelectionState());
8657       } else if (mode == Mode.PANNING) {
8658         setState(getPanningState());
8659       } else if (mode == Mode.WALL_CREATION) {
8660         setState(getWallCreationState());
8661       } else if (mode == Mode.ROOM_CREATION) {
8662         setState(getRoomCreationState());
8663       } else if (mode == Mode.POLYLINE_CREATION) {
8664         setState(getPolylineCreationState());
8665       } else if (mode == Mode.DIMENSION_LINE_CREATION) {
8666         setState(getDimensionLineCreationState());
8667       } else if (mode == Mode.LABEL_CREATION) {
8668         setState(getLabelCreationState());
8669       }
8670     }
8671 
8672     @Override
8673     public void deleteSelection() {
8674       deleteItems(home.getSelectedItems());
8675       // Compute again feedback
8676       moveMouse(getXLastMouseMove(), getYLastMouseMove());
8677     }
8678 
8679     @Override
8680     public void moveSelection(float dx, float dy) {
8681       moveAndShowSelectedItems(dx, dy);
8682       // Compute again feedback
8683       moveMouse(getXLastMouseMove(), getYLastMouseMove());
8684     }
8685 
8686     @Override
8687     public void zoom(float factor) {
8688       setScale(getScale() * factor);
8689     }
8690   }
8691 
8692   /**
8693    * Default selection state. This state manages transition to other modes,
8694    * the deletion of selected items, and the move of selected items with arrow keys.
8695    */
8696   private class SelectionState extends AbstractModeChangeState {
8697     private final SelectionListener selectionListener = new SelectionListener() {
8698         public void selectionChanged(SelectionEvent selectionEvent) {
8699           List<Selectable> selectedItems = home.getSelectedItems();
8700           getView().setResizeIndicatorVisible(selectedItems.size() == 1
8701                   && (isItemResizable(selectedItems.get(0))
8702                       || isItemMovable(selectedItems.get(0))));
8703         }
8704       };
8705 
8706     @Override
8707     public Mode getMode() {
8708       return Mode.SELECTION;
8709     }
8710 
8711     @Override
8712     public void enter() {
8713       if (getView() != null) {
8714         if (getPointerTypeLastMousePress() != View.PointerType.TOUCH) {
8715           moveMouse(getXLastMouseMove(), getYLastMouseMove());
8716         }
8717         home.addSelectionListener(this.selectionListener);
8718         this.selectionListener.selectionChanged(null);
8719       }
8720     }
8721 
8722     @Override
8723     public void moveMouse(float x, float y) {
8724       if (getRotatedLabelAt(x, y) != null
8725           || getYawRotatedCameraAt(x, y) != null
8726           || getPitchRotatedCameraAt(x, y) != null) {
8727         getView().setCursor(PlanView.CursorType.ROTATION);
8728       } else if (getElevatedLabelAt(x, y) != null
8729           || getElevatedCameraAt(x, y) != null) {
8730         getView().setCursor(PlanView.CursorType.ELEVATION);
8731       } else if (getRoomNameAt(x, y) != null
8732           || getRoomAreaAt(x, y) != null) {
8733         getView().setCursor(PlanView.CursorType.RESIZE);
8734       } else if (getRoomRotatedNameAt(x, y) != null
8735           || getRoomRotatedAreaAt(x, y) != null) {
8736         getView().setCursor(PlanView.CursorType.ROTATION);
8737       } else if (getResizedDimensionLineStartAt(x, y) != null
8738           || getResizedDimensionLineEndAt(x, y) != null
8739           || getWidthAndDepthResizedPieceOfFurnitureAt(x, y) != null
8740           || getResizedWallStartAt(x, y) != null
8741           || getResizedWallEndAt(x, y) != null
8742           || getResizedPolylineAt(x, y) != null
8743           || getResizedRoomAt(x, y) != null) {
8744         getView().setCursor(PlanView.CursorType.RESIZE);
8745       } else if (getPitchRotatedPieceOfFurnitureAt(x, y) != null
8746           || getRollRotatedPieceOfFurnitureAt(x, y) != null) {
8747         getView().setCursor(PlanView.CursorType.ROTATION);
8748       } else if (getModifiedLightPowerAt(x, y) != null) {
8749         getView().setCursor(PlanView.CursorType.POWER);
8750       } else if (getOffsetDimensionLineAt(x, y) != null
8751           || getHeightResizedPieceOfFurnitureAt(x, y) != null
8752           || getArcExtentWallAt(x, y) != null) {
8753         getView().setCursor(PlanView.CursorType.HEIGHT);
8754       } else if (getRotatedPieceOfFurnitureAt(x, y) != null) {
8755         getView().setCursor(PlanView.CursorType.ROTATION);
8756       } else if (getElevatedPieceOfFurnitureAt(x, y) != null) {
8757         getView().setCursor(PlanView.CursorType.ELEVATION);
8758       } else if (getPieceOfFurnitureNameAt(x, y) != null) {
8759         getView().setCursor(PlanView.CursorType.RESIZE);
8760       } else if (getPieceOfFurnitureRotatedNameAt(x, y) != null) {
8761         getView().setCursor(PlanView.CursorType.ROTATION);
8762       } else if (getRotatedCompassAt(x, y) != null) {
8763         getView().setCursor(PlanView.CursorType.ROTATION);
8764       } else if (getResizedCompassAt(x, y) != null) {
8765         getView().setCursor(PlanView.CursorType.RESIZE);
8766       } else {
8767         // If a selected item is under cursor position
8768         if (isItemSelectedAt(x, y)) {
8769           getView().setCursor(PlanView.CursorType.MOVE);
8770         } else {
8771           getView().setCursor(PlanView.CursorType.SELECTION);
8772         }
8773       }
8774     }
8775 
8776     @Override
8777     public void pressMouse(float x, float y, int clickCount,
8778                            boolean shiftDown, boolean duplicationActivated) {
8779       if (clickCount == 1) {
8780         if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
8781           moveMouse(x, y); // Update cursor
8782         }
8783         if (getRotatedLabelAt(x, y) != null) {
8784           setState(getLabelRotationState());
8785         } else if (getYawRotatedCameraAt(x, y) != null) {
8786           setState(getCameraYawRotationState());
8787         } else if (getPitchRotatedCameraAt(x, y) != null) {
8788           setState(getCameraPitchRotationState());
8789         } else if (getElevatedLabelAt(x, y) != null) {
8790           setState(getLabelElevationState());
8791         } else if (getElevatedCameraAt(x, y) != null) {
8792           setState(getCameraElevationState());
8793         } else if (getRoomNameAt(x, y) != null) {
8794           setState(getRoomNameOffsetState());
8795         } else if (getRoomRotatedNameAt(x, y) != null) {
8796           setState(getRoomNameRotationState());
8797         } else if (getRoomAreaAt(x, y) != null) {
8798           setState(getRoomAreaOffsetState());
8799         } else if (getRoomRotatedAreaAt(x, y) != null) {
8800           setState(getRoomAreaRotationState());
8801         } else if (getResizedDimensionLineStartAt(x, y) != null
8802             || getResizedDimensionLineEndAt(x, y) != null) {
8803           setState(getDimensionLineResizeState());
8804         } else if (getWidthAndDepthResizedPieceOfFurnitureAt(x, y) != null) {
8805           setState(getPieceOfFurnitureResizeState());
8806         } else if (getResizedWallStartAt(x, y) != null
8807             || getResizedWallEndAt(x, y) != null) {
8808           setState(getWallResizeState());
8809         } else if (getResizedRoomAt(x, y) != null) {
8810           setState(getRoomResizeState());
8811         } else if (getOffsetDimensionLineAt(x, y) != null) {
8812           setState(getDimensionLineOffsetState());
8813         } else if (getResizedPolylineAt(x, y) != null) {
8814           setState(getPolylineResizeState());
8815         } else if (getPitchRotatedPieceOfFurnitureAt(x, y) != null) {
8816           setState(getPieceOfFurniturePitchRotationState());
8817         } else if (getRollRotatedPieceOfFurnitureAt(x, y) != null) {
8818           setState(getPieceOfFurnitureRollRotationState());
8819         } else if (getModifiedLightPowerAt(x, y) != null) {
8820           setState(getLightPowerModificationState());
8821         } else if (getHeightResizedPieceOfFurnitureAt(x, y) != null) {
8822           setState(getPieceOfFurnitureHeightState());
8823         } else if (getArcExtentWallAt(x, y) != null) {
8824           setState(getWallArcExtentState());
8825         } else if (getRotatedPieceOfFurnitureAt(x, y) != null) {
8826           setState(getPieceOfFurnitureRotationState());
8827         } else if (getElevatedPieceOfFurnitureAt(x, y) != null) {
8828           setState(getPieceOfFurnitureElevationState());
8829         } else if (getPieceOfFurnitureNameAt(x, y) != null) {
8830           setState(getPieceOfFurnitureNameOffsetState());
8831         } else if (getPieceOfFurnitureRotatedNameAt(x, y) != null) {
8832           setState(getPieceOfFurnitureNameRotationState());
8833         } else if (getRotatedCompassAt(x, y) != null) {
8834           setState(getCompassRotationState());
8835         } else if (getResizedCompassAt(x, y) != null) {
8836           setState(getCompassResizeState());
8837         } else {
8838           // If shift isn't pressed, and an item is under cursor position
8839           if (!shiftDown
8840               && (getPointerTypeLastMousePress() == View.PointerType.TOUCH
8841                   || getSelectableItemAt(x, y) != null)) {
8842             // Change state to SelectionMoveState
8843             setState(getSelectionMoveState());
8844           } else {
8845             // Otherwise change state to RectangleSelectionState
8846             setState(getRectangleSelectionState());
8847           }
8848         }
8849       } else if (clickCount == 2) {
8850         Selectable item = getSelectableItemAt(x, y);
8851         // If shift isn't pressed, and an item is under cursor position
8852         if (!shiftDown && item != null) {
8853           // Modify selected item on a double click
8854           if (item instanceof Wall) {
8855             modifySelectedWalls();
8856           } else if (item instanceof HomePieceOfFurniture) {
8857             modifySelectedFurniture();
8858           } else if (item instanceof Room) {
8859             modifySelectedRooms();
8860           } else if (item instanceof Polyline) {
8861             modifySelectedPolylines();
8862           } else if (item instanceof Label) {
8863             modifySelectedLabels();
8864           } else if (item instanceof Compass) {
8865             modifyCompass();
8866           } else if (item instanceof ObserverCamera) {
8867             modifyObserverCamera();
8868           }
8869         }
8870       }
8871     }
8872 
8873     @Override
8874     public void exit() {
8875       if (getView() != null) {
8876         home.removeSelectionListener(this.selectionListener);
8877         getView().setResizeIndicatorVisible(false);
8878       }
8879     }
8880   }
8881 
8882   /**
8883    * Move selection state. This state manages the move of current selected items
8884    * with mouse and the selection of one item, if mouse isn't moved while button
8885    * is depressed. If duplication is activated during the move of the mouse,
8886    * moved items are duplicated first.
8887    */
8888   private class SelectionMoveState extends ControllerState {
8889     private float                xLastMouseMove;
8890     private float                yLastMouseMove;
8891     private boolean              mouseMoved;
8892     private List<Selectable>     oldSelection;
8893     private boolean              selectionUpdateNeeded;
8894     private List<Selectable>     movedItems;
8895     private List<Selectable>     duplicatedItems;
8896     private HomePieceOfFurniture movedPieceOfFurniture;
8897     private float                angleMovedPieceOfFurniture;
8898     private float                depthMovedPieceOfFurniture;
8899     private float                elevationMovedPieceOfFurniture;
8900     private float                xMovedPieceOfFurniture;
8901     private float                yMovedPieceOfFurniture;
8902     private boolean              movedDoorOrWindowBoundToWall;
8903     private boolean              magnetismEnabled;
8904     private boolean              duplicationActivated;
8905     private boolean              alignmentActivated;
8906     private boolean              basePlanModification;
8907 
8908     @Override
8909     public Mode getMode() {
8910       return Mode.SELECTION;
8911     }
8912 
8913     @Override
8914     public boolean isModificationState() {
8915       return true;
8916     }
8917 
8918     @Override
8919     public boolean isBasePlanModificationState() {
8920       return this.basePlanModification;
8921     }
8922 
8923     @Override
8924     public void enter() {
8925       this.xLastMouseMove = getXLastMousePress();
8926       this.yLastMouseMove = getYLastMousePress();
8927       this.mouseMoved = false;
8928       List<Selectable> selectableItemsUnderCursor = getSelectableItemsAt(getXLastMousePress(), getYLastMousePress());
8929       List<Selectable> selectableItemsAndGroupsFurnitureUnderCursor = new ArrayList<Selectable>(selectableItemsUnderCursor);
8930       float selectionMargin = getSelectionMargin();
8931       for (Selectable item : selectableItemsUnderCursor) {
8932         if (item instanceof HomeFurnitureGroup) {
8933           for (HomePieceOfFurniture piece : ((HomeFurnitureGroup)item).getAllFurniture()) {
8934             if (piece.containsPoint(getXLastMousePress(), getYLastMousePress(), selectionMargin)) {
8935               selectableItemsAndGroupsFurnitureUnderCursor.add(piece);
8936             }
8937           }
8938         }
8939       }
8940       this.oldSelection = home.getSelectedItems();
8941       toggleMagnetism(wasMagnetismToggledLastMousePress());
8942       this.selectionUpdateNeeded = Collections.disjoint(selectableItemsAndGroupsFurnitureUnderCursor, this.oldSelection);
8943       if (this.selectionUpdateNeeded
8944           && getPointerTypeLastMousePress() != View.PointerType.TOUCH) {
8945         // Select only the item with highest priority under cursor position
8946         selectItem(getSelectableItemAt(getXLastMousePress(), getYLastMousePress(), false));
8947         // In touch environment, wait either for mouse move to switch to panning state
8948         // or mouse release to update the selection
8949       }
8950       List<Selectable> selectedItems = home.getSelectedItems();
8951       this.movedItems = new ArrayList<Selectable>(selectedItems.size());
8952       this.basePlanModification = false;
8953       for (Selectable item : selectedItems) {
8954         if (isItemMovable(item)) {
8955           this.movedItems.add(item);
8956           if (!this.basePlanModification
8957               && isItemPartOfBasePlan(item)) {
8958             this.basePlanModification = true;
8959           }
8960         }
8961       }
8962       if (this.movedItems.size() == 1
8963           && this.movedItems.get(0) instanceof HomePieceOfFurniture) {
8964         this.movedPieceOfFurniture = (HomePieceOfFurniture)this.movedItems.get(0);
8965         this.xMovedPieceOfFurniture = this.movedPieceOfFurniture.getX();
8966         this.yMovedPieceOfFurniture = this.movedPieceOfFurniture.getY();
8967         this.angleMovedPieceOfFurniture = this.movedPieceOfFurniture.getAngle();
8968         this.depthMovedPieceOfFurniture = this.movedPieceOfFurniture.getDepth();
8969         this.elevationMovedPieceOfFurniture = this.movedPieceOfFurniture.getElevation();
8970         this.movedDoorOrWindowBoundToWall = this.movedPieceOfFurniture instanceof HomeDoorOrWindow
8971             && ((HomeDoorOrWindow)this.movedPieceOfFurniture).isBoundToWall();
8972       }
8973       this.duplicatedItems = null;
8974       this.duplicationActivated = wasDuplicationActivatedLastMousePress() && !home.isAllLevelsSelection();
8975       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
8976       if (!(getPointerTypeLastMousePress() == View.PointerType.TOUCH
8977             && this.selectionUpdateNeeded)) {
8978         getView().setCursor(PlanView.CursorType.MOVE);
8979       }
8980     }
8981 
8982     @Override
8983     public void moveMouse(float x, float y) {
8984       if (getPointerTypeLastMousePress() == View.PointerType.TOUCH
8985           && this.selectionUpdateNeeded) {
8986         // When the user touched an unselected item
8987         // go to panning state for one shot, returning to selection state once panning is done
8988         setState(new ControllerStateDecorator(getPanningState()) {
8989             @Override
8990             public void enter() {
8991               super.enter();
8992               pressMouse(getXLastMousePress(), getYLastMousePress(), 1, wasShiftDownLastMousePress(), wasDuplicationActivatedLastMousePress());
8993               moveMouse(getXLastMouseMove(), getYLastMouseMove());
8994             }
8995 
8996             @Override
8997             public void releaseMouse(float x, float y) {
8998               escape();
8999             }
9000 
9001             @Override
9002             public void escape() {
9003               super.escape();
9004               setState(getSelectionState());
9005             }
9006           });
9007       } else {
9008         if (!this.mouseMoved) {
9009           toggleDuplication(this.duplicationActivated);
9010         }
9011         if (this.alignmentActivated) {
9012           PointWithAngleMagnetism alignedPoint = new PointWithAngleMagnetism(getXLastMousePress(), getYLastMousePress(),
9013               x, y, preferences.getLengthUnit(), getView().getPixelLength(), 4);
9014           x = alignedPoint.getX();
9015           y = alignedPoint.getY();
9016         }
9017         if (this.movedPieceOfFurniture != null) {
9018           // Reset to default piece values and adjust piece of furniture location, angle and depth
9019           this.movedPieceOfFurniture.setX(this.xMovedPieceOfFurniture);
9020           this.movedPieceOfFurniture.setY(this.yMovedPieceOfFurniture);
9021           this.movedPieceOfFurniture.setAngle(this.angleMovedPieceOfFurniture);
9022           if (this.movedPieceOfFurniture instanceof HomeDoorOrWindow
9023               && this.movedPieceOfFurniture.isResizable()
9024               && isItemResizable(this.movedPieceOfFurniture)) {
9025             this.movedPieceOfFurniture.setDepth(this.depthMovedPieceOfFurniture);
9026           }
9027           this.movedPieceOfFurniture.setElevation(this.elevationMovedPieceOfFurniture);
9028           this.movedPieceOfFurniture.move(x - getXLastMousePress(), y - getYLastMousePress());
9029           if (this.magnetismEnabled && !this.alignmentActivated) {
9030             boolean elevationAdjusted = adjustPieceOfFurnitureElevation(this.movedPieceOfFurniture) != null;
9031             Wall magnetWall = adjustPieceOfFurnitureOnWallAt(this.movedPieceOfFurniture, x, y, false);
9032             if (!elevationAdjusted) {
9033               adjustPieceOfFurnitureSideBySideAt(this.movedPieceOfFurniture, false, magnetWall);
9034             }
9035             if (magnetWall != null) {
9036               getView().setDimensionLinesFeedback(getDimensionLinesAlongWall(this.movedPieceOfFurniture, magnetWall));
9037             } else {
9038               getView().setDimensionLinesFeedback(null);
9039             }
9040           }
9041         } else {
9042           moveItems(this.movedItems, x - this.xLastMouseMove, y - this.yLastMouseMove);
9043         }
9044 
9045         if (!this.mouseMoved) {
9046           selectItems(this.movedItems, home.isAllLevelsSelection());
9047         }
9048         getView().makePointVisible(x, y);
9049         this.xLastMouseMove = x;
9050         this.yLastMouseMove = y;
9051         this.mouseMoved = true;
9052       }
9053     }
9054 
9055     @Override
9056     public void releaseMouse(float x, float y) {
9057       if (this.mouseMoved) {
9058         // Post in undo support a move or duplicate operation if selection isn't a camera
9059         if (this.movedItems.size() > 0
9060             && !(this.movedItems.get(0) instanceof Camera)) {
9061           if (this.duplicatedItems != null) {
9062             postItemsDuplication(this.movedItems, this.duplicatedItems);
9063           } else if (this.movedPieceOfFurniture != null) {
9064             postPieceOfFurnitureMove(this.movedPieceOfFurniture,
9065                 this.movedPieceOfFurniture.getX() - this.xMovedPieceOfFurniture,
9066                 this.movedPieceOfFurniture.getY() - this.yMovedPieceOfFurniture,
9067                 this.angleMovedPieceOfFurniture,
9068                 this.depthMovedPieceOfFurniture,
9069                 this.elevationMovedPieceOfFurniture,
9070                 this.movedDoorOrWindowBoundToWall);
9071           } else {
9072             postItemsMove(this.movedItems, this.oldSelection,
9073                 this.xLastMouseMove - getXLastMousePress(),
9074                 this.yLastMouseMove - getYLastMousePress());
9075           }
9076         }
9077       } else {
9078         // If mouse didn't move, select only the item at (x,y)
9079         if (getPointerTypeLastMousePress() == View.PointerType.TOUCH
9080             || !Collections.disjoint(home.getSelectedItems(), this.oldSelection)) {
9081           Selectable itemUnderCursor = getSelectableItemAt(x, y, false);
9082           if (itemUnderCursor != null) {
9083             // Select only the item under cursor position
9084             selectItem(itemUnderCursor);
9085           } else {
9086             // May happen only in touch environment
9087             deselectAll();
9088           }
9089         }
9090       }
9091       // Change the state to SelectionState
9092       setState(getSelectionState());
9093     }
9094 
9095     @Override
9096     public void toggleMagnetism(boolean magnetismToggled) {
9097       // Compute active magnetism
9098       this.magnetismEnabled = preferences.isMagnetismEnabled()
9099                               ^ magnetismToggled;
9100       // Compute again piece move as if mouse moved
9101       if (this.movedPieceOfFurniture != null) {
9102         moveMouse(getXLastMouseMove(), getYLastMouseMove());
9103         if (!this.magnetismEnabled) {
9104           getView().deleteFeedback();
9105         }
9106       }
9107     }
9108 
9109     @Override
9110     public void escape() {
9111       if (this.mouseMoved) {
9112         if (this.duplicatedItems != null) {
9113           // Delete moved items and select original items
9114           doDeleteItems(this.movedItems);
9115           selectItems(this.duplicatedItems);
9116         } else {
9117           // Put items back to their initial location
9118           if (this.movedPieceOfFurniture != null) {
9119             this.movedPieceOfFurniture.setX(this.xMovedPieceOfFurniture);
9120             this.movedPieceOfFurniture.setY(this.yMovedPieceOfFurniture);
9121             this.movedPieceOfFurniture.setAngle(this.angleMovedPieceOfFurniture);
9122             if (this.movedPieceOfFurniture instanceof HomeDoorOrWindow
9123                 && this.movedPieceOfFurniture.isResizable()
9124                 && isItemResizable(this.movedPieceOfFurniture)) {
9125               this.movedPieceOfFurniture.setDepth(this.depthMovedPieceOfFurniture);
9126             }
9127             this.movedPieceOfFurniture.setElevation(this.elevationMovedPieceOfFurniture);
9128             if (this.movedPieceOfFurniture instanceof HomeDoorOrWindow) {
9129               ((HomeDoorOrWindow)this.movedPieceOfFurniture).setBoundToWall(
9130                   this.movedDoorOrWindowBoundToWall);
9131             }
9132           } else {
9133             moveItems(this.movedItems,
9134                 getXLastMousePress() - this.xLastMouseMove,
9135                 getYLastMousePress() - this.yLastMouseMove);
9136           }
9137         }
9138       }
9139       // Change the state to SelectionState
9140       setState(getSelectionState());
9141     }
9142 
9143     @Override
9144     public void setDuplicationActivated(boolean duplicationActivated) {
9145       duplicationActivated &= !home.isAllLevelsSelection();
9146       if (this.mouseMoved) {
9147         toggleDuplication(duplicationActivated);
9148       }
9149       this.duplicationActivated = duplicationActivated;
9150     }
9151 
9152     @Override
9153     public void setAlignmentActivated(boolean alignmentActivated) {
9154       if (getPointerTypeLastMousePress() == View.PointerType.TOUCH
9155           && this.selectionUpdateNeeded
9156           && !this.duplicationActivated) {
9157         // When the user touched an unselected item and simulated a shift press
9158         // go back to selection state, simulating a click with shift key pressed
9159         setState(getSelectionState());
9160         PlanController.this.pressMouse(getXLastMousePress(), getYLastMousePress(), 1, true, false, false, false, getPointerTypeLastMousePress());
9161       } else {
9162         this.alignmentActivated = alignmentActivated;
9163         if (this.mouseMoved) {
9164           moveMouse(getXLastMouseMove(), getYLastMouseMove());
9165         }
9166         if (this.alignmentActivated) {
9167           getView().deleteFeedback();
9168         }
9169       }
9170     }
9171 
9172     private void toggleDuplication(boolean duplicationActivated) {
9173       if (this.movedItems.size() > 1
9174           || (this.movedItems.size() == 1
9175               && !(this.movedItems.get(0) instanceof Camera)
9176               && !(this.movedItems.get(0) instanceof Compass))) {
9177         if (duplicationActivated
9178             && this.duplicatedItems == null) {
9179           // Duplicate original items and add them to home
9180           this.duplicatedItems = this.movedItems;
9181           this.movedItems = new ArrayList<Selectable>();
9182           for (Selectable item : Home.duplicate(this.duplicatedItems)) {
9183             if (item instanceof Wall) {
9184               home.addWall((Wall)item);
9185             } else if (item instanceof Room) {
9186               home.addRoom((Room)item);
9187             } else if (item instanceof Polyline) {
9188               home.addPolyline((Polyline)item);
9189             } else if (item instanceof DimensionLine) {
9190               home.addDimensionLine((DimensionLine)item);
9191             } else if (item instanceof HomePieceOfFurniture) {
9192               home.addPieceOfFurniture((HomePieceOfFurniture)item);
9193             } else if (item instanceof Label) {
9194               home.addLabel((Label)item);
9195             } else {
9196               continue;
9197             }
9198             this.movedItems.add(item);
9199           }
9200 
9201           // Put original items back to their initial location
9202           if (this.movedPieceOfFurniture != null) {
9203             this.movedPieceOfFurniture.setX(this.xMovedPieceOfFurniture);
9204             this.movedPieceOfFurniture.setY(this.yMovedPieceOfFurniture);
9205             this.movedPieceOfFurniture.setAngle(this.angleMovedPieceOfFurniture);
9206             if (this.movedPieceOfFurniture instanceof HomeDoorOrWindow
9207                 && this.movedPieceOfFurniture.isResizable()
9208                 && isItemResizable(this.movedPieceOfFurniture)) {
9209               this.movedPieceOfFurniture.setDepth(this.depthMovedPieceOfFurniture);
9210             }
9211             this.movedPieceOfFurniture.setElevation(this.elevationMovedPieceOfFurniture);
9212             this.movedPieceOfFurniture = (HomePieceOfFurniture)this.movedItems.get(0);
9213           } else {
9214             moveItems(this.duplicatedItems,
9215                 getXLastMousePress() - this.xLastMouseMove,
9216                 getYLastMousePress() - this.yLastMouseMove);
9217           }
9218 
9219           getView().setCursor(PlanView.CursorType.DUPLICATION);
9220         } else if (!duplicationActivated
9221                    && this.duplicatedItems != null) {
9222           // Delete moved items
9223           doDeleteItems(this.movedItems);
9224 
9225           // Move original items to the current location
9226           moveItems(this.duplicatedItems,
9227               this.xLastMouseMove - getXLastMousePress(),
9228               this.yLastMouseMove - getYLastMousePress());
9229           this.movedItems = this.duplicatedItems;
9230           this.duplicatedItems = null;
9231           if (this.movedPieceOfFurniture != null) {
9232             this.movedPieceOfFurniture = (HomePieceOfFurniture)this.movedItems.get(0);
9233           }
9234           getView().setCursor(PlanView.CursorType.MOVE);
9235         }
9236 
9237         selectItems(this.movedItems, home.isAllLevelsSelection());
9238       }
9239     }
9240 
9241     @Override
9242     public void exit() {
9243       getView().deleteFeedback();
9244       this.movedItems = null;
9245       this.duplicatedItems = null;
9246       this.movedPieceOfFurniture = null;
9247     }
9248   }
9249 
9250   /**
9251    * Selection with rectangle state. This state manages selection when mouse
9252    * press is done outside of an item or when mouse press is done with shift key
9253    * down.
9254    */
9255   private class RectangleSelectionState extends ControllerState {
9256     private List<Selectable> selectedItemsMousePressed;
9257     private boolean          ignoreRectangleSelection;
9258     private boolean          mouseMoved;
9259 
9260     @Override
9261     public Mode getMode() {
9262       return Mode.SELECTION;
9263     }
9264 
9265     @Override
9266     public boolean isModificationState() {
9267       return true;
9268     }
9269 
9270     @Override
9271     public void enter() {
9272       Selectable itemUnderCursor = getSelectableItemAt(getXLastMousePress(), getYLastMousePress());
9273       // If no item under cursor and shift wasn't down, deselect all
9274       if (itemUnderCursor == null && !wasShiftDownLastMousePress()) {
9275         deselectAll();
9276       }
9277       // Store current selection
9278       this.selectedItemsMousePressed = new ArrayList<Selectable>(home.getSelectedItems());
9279       List<HomePieceOfFurniture> furniture = home.getFurniture();
9280       this.ignoreRectangleSelection = false;
9281       for (Selectable item : this.selectedItemsMousePressed) {
9282         if ((item instanceof HomePieceOfFurniture)
9283             && !furniture.contains(item)) {
9284           this.ignoreRectangleSelection = true;
9285           break;
9286         }
9287       }
9288       this.mouseMoved = false;
9289     }
9290 
9291     @Override
9292     public void moveMouse(float x, float y) {
9293       this.mouseMoved = true;
9294       if (!this.ignoreRectangleSelection) {
9295         updateSelectedItems(getXLastMousePress(), getYLastMousePress(),
9296             x, y, this.selectedItemsMousePressed);
9297         // Update rectangle feedback
9298         PlanView planView = getView();
9299         planView.setRectangleFeedback(
9300             getXLastMousePress(), getYLastMousePress(), x, y);
9301         planView.makePointVisible(x, y);
9302       }
9303     }
9304 
9305     @Override
9306     public void releaseMouse(float x, float y) {
9307       // If cursor didn't move
9308       if (!this.mouseMoved) {
9309         Selectable itemUnderCursor = getSelectableItemAt(x, y, false);
9310         // Toggle selection of the item under cursor
9311         if (itemUnderCursor != null) {
9312           if (this.selectedItemsMousePressed.contains(itemUnderCursor)) {
9313             this.selectedItemsMousePressed.remove(itemUnderCursor);
9314           } else {
9315             for (int i = this.selectedItemsMousePressed.size() - 1; i >= 0; i--) {
9316               // Remove any camera or group of a selected piece from current selection
9317               Selectable item = this.selectedItemsMousePressed.get(i);
9318               if (item instanceof Camera
9319                   || (itemUnderCursor instanceof HomePieceOfFurniture
9320                       && item instanceof HomeFurnitureGroup
9321                       && ((HomeFurnitureGroup)item).getAllFurniture().contains(itemUnderCursor))
9322                   || (itemUnderCursor instanceof HomeFurnitureGroup
9323                       && item instanceof HomePieceOfFurniture
9324                       && ((HomeFurnitureGroup)itemUnderCursor).getAllFurniture().contains(item))) {
9325                 this.selectedItemsMousePressed.remove(i);
9326               }
9327             }
9328             // Let the camera belong to selection only if no item are selected
9329             if (!(itemUnderCursor instanceof Camera)
9330                 || this.selectedItemsMousePressed.size() == 0) {
9331               this.selectedItemsMousePressed.add(itemUnderCursor);
9332             }
9333           }
9334           selectItems(this.selectedItemsMousePressed,
9335               home.isAllLevelsSelection() && wasShiftDownLastMousePress());
9336         }
9337       }
9338       // Change state to SelectionState
9339       setState(getSelectionState());
9340     }
9341 
9342     @Override
9343     public void escape() {
9344       setState(getSelectionState());
9345     }
9346 
9347     @Override
9348     public void exit() {
9349       getView().deleteFeedback();
9350       this.selectedItemsMousePressed = null;
9351     }
9352 
9353     /**
9354      * Updates selection from <code>selectedItemsMousePressed</code> and the
9355      * items that intersects the rectangle at coordinates (<code>x0</code>,
9356      * <code>y0</code>) and (<code>x1</code>, <code>y1</code>).
9357      */
9358     private void updateSelectedItems(float x0, float y0,
9359                                      float x1, float y1,
9360                                      List<Selectable> selectedItemsMousePressed) {
9361       List<Selectable> selectedItems;
9362       boolean shiftDown = wasShiftDownLastMousePress();
9363       if (shiftDown) {
9364         selectedItems = new ArrayList<Selectable>(selectedItemsMousePressed);
9365       } else {
9366         selectedItems = new ArrayList<Selectable>();
9367       }
9368 
9369       // For all the items that intersect with rectangle
9370       for (Selectable item : getSelectableItemsIntersectingRectangle(x0, y0, x1, y1)) {
9371         // Don't let the camera be able to be selected with a rectangle
9372         if (!(item instanceof Camera)) {
9373           // If shift was down at mouse press
9374           if (shiftDown) {
9375             // Toggle selection of item
9376             if (selectedItemsMousePressed.contains(item)) {
9377               selectedItems.remove(item);
9378             } else {
9379               selectedItems.add(item);
9380             }
9381           } else if (!selectedItemsMousePressed.contains(item)) {
9382             // Else select the wall
9383             selectedItems.add(item);
9384           }
9385         }
9386       }
9387       // Update selection
9388       selectItems(selectedItems, home.isAllLevelsSelection() && shiftDown);
9389     }
9390   }
9391 
9392   /**
9393    * Panning state.
9394    */
9395   private class PanningState extends ControllerState {
9396     private Integer xLastMouseMove;
9397     private Integer yLastMouseMove;
9398 
9399     @Override
9400     public Mode getMode() {
9401       return Mode.PANNING;
9402     }
9403 
9404     @Override
9405     public void setMode(Mode mode) {
9406       if (mode == Mode.SELECTION) {
9407         setState(getSelectionState());
9408       } else if (mode == Mode.WALL_CREATION) {
9409         setState(getWallCreationState());
9410       } else if (mode == Mode.ROOM_CREATION) {
9411         setState(getRoomCreationState());
9412       } else if (mode == Mode.POLYLINE_CREATION) {
9413         setState(getPolylineCreationState());
9414       } else if (mode == Mode.DIMENSION_LINE_CREATION) {
9415         setState(getDimensionLineCreationState());
9416       } else if (mode == Mode.LABEL_CREATION) {
9417         setState(getLabelCreationState());
9418       }
9419     }
9420 
9421     @Override
9422     public void enter() {
9423       getView().setCursor(PlanView.CursorType.PANNING);
9424     }
9425 
9426     @Override
9427     public void moveSelection(float dx, float dy) {
9428       getView().moveView(dx * 10, dy * 10);
9429     }
9430 
9431     @Override
9432     public void pressMouse(float x, float y, int clickCount, boolean shiftDown, boolean duplicationActivated) {
9433       if (clickCount == 1) {
9434         this.xLastMouseMove = getView().convertXModelToScreen(x);
9435         this.yLastMouseMove = getView().convertYModelToScreen(y);
9436       } else {
9437         this.xLastMouseMove = null;
9438         this.yLastMouseMove = null;
9439       }
9440     }
9441 
9442     @Override
9443     public void moveMouse(float x, float y) {
9444       if (this.xLastMouseMove != null) {
9445         int newX = getView().convertXModelToScreen(x);
9446         int newY = getView().convertYModelToScreen(y);
9447         getView().moveView((this.xLastMouseMove - newX) / getScale(), (this.yLastMouseMove - newY) / getScale());
9448         this.xLastMouseMove = newX;
9449         this.yLastMouseMove = newY;
9450       }
9451     }
9452 
9453     @Override
9454     public void releaseMouse(float x, float y) {
9455       this.xLastMouseMove = null;
9456     }
9457 
9458     @Override
9459     public void escape() {
9460       this.xLastMouseMove = null;
9461     }
9462 
9463     @Override
9464     public void zoom(float factor) {
9465       setScale(getScale() * factor);
9466     }
9467   }
9468 
9469   /**
9470    * Drag and drop state. This state manages the dragging of items
9471    * transfered from outside of plan view with the mouse.
9472    */
9473   private class DragAndDropState extends ControllerState {
9474     private float                xLastMouseMove;
9475     private float                yLastMouseMove;
9476     private HomePieceOfFurniture draggedPieceOfFurniture;
9477     private float                xDraggedPieceOfFurniture;
9478     private float                yDraggedPieceOfFurniture;
9479     private float                angleDraggedPieceOfFurniture;
9480     private float                depthDraggedPieceOfFurniture;
9481     private float                elevationDraggedPieceOfFurniture;
9482 
9483     @Override
9484     public Mode getMode() {
9485       return Mode.SELECTION;
9486     }
9487 
9488     @Override
9489     public boolean isModificationState() {
9490       // This state is used before a modification is performed
9491       return false;
9492     }
9493 
9494     @Override
9495     public boolean isBasePlanModificationState() {
9496       return this.draggedPieceOfFurniture != null
9497           && isPieceOfFurniturePartOfBasePlan(this.draggedPieceOfFurniture);
9498     }
9499 
9500     @Override
9501     public void enter() {
9502       this.xLastMouseMove = 0;
9503       this.yLastMouseMove = 0;
9504       getView().setDraggedItemsFeedback(draggedItems);
9505       if (draggedItems.size() == 1
9506           && draggedItems.get(0) instanceof HomePieceOfFurniture) {
9507         this.draggedPieceOfFurniture = (HomePieceOfFurniture)draggedItems.get(0);
9508         this.xDraggedPieceOfFurniture = this.draggedPieceOfFurniture.getX();
9509         this.yDraggedPieceOfFurniture = this.draggedPieceOfFurniture.getY();
9510         this.angleDraggedPieceOfFurniture = this.draggedPieceOfFurniture.getAngle();
9511         this.depthDraggedPieceOfFurniture = this.draggedPieceOfFurniture.getDepth();
9512         this.elevationDraggedPieceOfFurniture = this.draggedPieceOfFurniture.getElevation();
9513       }
9514     }
9515 
9516     @Override
9517     public void moveMouse(float x, float y) {
9518       List<Selectable> draggedItemsFeedback = new ArrayList<Selectable>(draggedItems);
9519       // Update in plan view the location of the feedback of the dragged items
9520       moveItems(draggedItems, x - this.xLastMouseMove, y - this.yLastMouseMove);
9521       if (this.draggedPieceOfFurniture != null
9522           && preferences.isMagnetismEnabled()) {
9523         // Reset to default piece values and adjust piece of furniture location, angle and depth
9524         this.draggedPieceOfFurniture.setX(this.xDraggedPieceOfFurniture);
9525         this.draggedPieceOfFurniture.setY(this.yDraggedPieceOfFurniture);
9526         this.draggedPieceOfFurniture.setAngle(this.angleDraggedPieceOfFurniture);
9527         if (this.draggedPieceOfFurniture.isResizable()) {
9528           // Update of depth may happen only for doors and windows which can't be rotated around horizontal axes
9529           this.draggedPieceOfFurniture.setDepth(this.depthDraggedPieceOfFurniture);
9530           this.draggedPieceOfFurniture.setDepthInPlan(this.depthDraggedPieceOfFurniture);
9531         }
9532         this.draggedPieceOfFurniture.setElevation(this.elevationDraggedPieceOfFurniture);
9533         this.draggedPieceOfFurniture.move(x, y);
9534 
9535         boolean elevationAdjusted = adjustPieceOfFurnitureElevation(this.draggedPieceOfFurniture) != null;
9536         Wall magnetWall = adjustPieceOfFurnitureOnWallAt(this.draggedPieceOfFurniture, x, y, true);
9537         if (!elevationAdjusted) {
9538           adjustPieceOfFurnitureSideBySideAt(this.draggedPieceOfFurniture, magnetWall == null, magnetWall);
9539         }
9540         if (magnetWall != null) {
9541           getView().setDimensionLinesFeedback(getDimensionLinesAlongWall(this.draggedPieceOfFurniture, magnetWall));
9542         } else {
9543           getView().setDimensionLinesFeedback(null);
9544         }
9545       }
9546       getView().setDraggedItemsFeedback(draggedItemsFeedback);
9547       this.xLastMouseMove = x;
9548       this.yLastMouseMove = y;
9549     }
9550 
9551     @Override
9552     public void exit() {
9553       this.draggedPieceOfFurniture = null;
9554       getView().deleteFeedback();
9555     }
9556   }
9557 
9558   /**
9559    * Wall creation state. This state manages transition to other modes,
9560    * and initial wall creation.
9561    */
9562   private class WallCreationState extends AbstractModeChangeState {
9563     private boolean magnetismEnabled;
9564 
9565     @Override
9566     public Mode getMode() {
9567       return Mode.WALL_CREATION;
9568     }
9569 
9570     @Override
9571     public void enter() {
9572       getView().setCursor(PlanView.CursorType.DRAW);
9573       toggleMagnetism(wasMagnetismToggledLastMousePress());
9574     }
9575 
9576     @Override
9577     public void moveMouse(float x, float y) {
9578       if (this.magnetismEnabled) {
9579         WallPointWithAngleMagnetism point = new WallPointWithAngleMagnetism(null, x, y, x, y);
9580         x = point.getX();
9581         y = point.getY();
9582       }
9583       getView().setAlignmentFeedback(Wall.class, null, x, y, false);
9584     }
9585 
9586     @Override
9587     public void pressMouse(float x, float y, int clickCount,
9588                            boolean shiftDown, boolean duplicationActivated) {
9589       // Change state to WallDrawingState
9590       setState(getWallDrawingState());
9591     }
9592 
9593     @Override
9594     public void setEditionActivated(boolean editionActivated) {
9595       if (editionActivated) {
9596         setState(getWallDrawingState());
9597         PlanController.this.setEditionActivated(editionActivated);
9598       }
9599     }
9600 
9601     @Override
9602     public void toggleMagnetism(boolean magnetismToggled) {
9603       // Compute active magnetism
9604       this.magnetismEnabled = preferences.isMagnetismEnabled()
9605                               ^ magnetismToggled;
9606       if (getPointerTypeLastMousePress() != View.PointerType.TOUCH) {
9607         moveMouse(getXLastMouseMove(), getYLastMouseMove());
9608       }
9609     }
9610 
9611     @Override
9612     public void exit() {
9613       getView().deleteFeedback();
9614     }
9615   }
9616 
9617   /**
9618    * Wall modification state.
9619    */
9620   private abstract class AbstractWallState extends ControllerState {
9621     private String wallLengthToolTipFeedback;
9622     private String wallAngleToolTipFeedback;
9623     private String wallArcExtentToolTipFeedback;
9624     private String wallThicknessToolTipFeedback;
9625 
9626     @Override
9627     public void enter() {
9628       this.wallLengthToolTipFeedback = preferences.getLocalizedString(
9629           PlanController.class, "wallLengthToolTipFeedback");
9630       try {
9631         this.wallAngleToolTipFeedback = preferences.getLocalizedString(
9632             PlanController.class, "wallAngleToolTipFeedback");
9633       } catch (IllegalArgumentException ex) {
9634         // This tool tip part is optional
9635       }
9636       this.wallArcExtentToolTipFeedback = preferences.getLocalizedString(
9637           PlanController.class, "wallArcExtentToolTipFeedback");
9638       try {
9639         this.wallThicknessToolTipFeedback = preferences.getLocalizedString(
9640             PlanController.class, "wallThicknessToolTipFeedback");
9641       } catch (IllegalArgumentException ex) {
9642         // This tool tip part is optional
9643       }
9644     }
9645 
9646     protected String getToolTipFeedbackText(Wall wall, boolean ignoreArcExtent) {
9647       Float arcExtent = wall.getArcExtent();
9648       if (!ignoreArcExtent && arcExtent != null) {
9649         return "<html>" + String.format(this.wallArcExtentToolTipFeedback, Math.round(Math.toDegrees(arcExtent)));
9650       } else {
9651         float startPointToEndPointDistance = wall.getStartPointToEndPointDistance();
9652         String toolTipFeedbackText = "<html>" + String.format(this.wallLengthToolTipFeedback, preferences.getLengthUnit().getFormatWithUnit().format(startPointToEndPointDistance));
9653         if (this.wallAngleToolTipFeedback != null
9654             && this.wallAngleToolTipFeedback.length() > 0) {
9655           toolTipFeedbackText += "<br>" + String.format(this.wallAngleToolTipFeedback, getWallAngleInDegrees(wall, startPointToEndPointDistance));
9656         }
9657         if (this.wallThicknessToolTipFeedback != null
9658             && this.wallThicknessToolTipFeedback.length() > 0) {
9659           toolTipFeedbackText += "<br>" + String.format(this.wallThicknessToolTipFeedback, preferences.getLengthUnit().getFormatWithUnit().format(wall.getThickness()));
9660         }
9661         return toolTipFeedbackText;
9662       }
9663     }
9664 
9665     /**
9666      * Returns wall angle in degrees.
9667      */
9668     protected Integer getWallAngleInDegrees(Wall wall) {
9669       return getWallAngleInDegrees(wall, wall.getStartPointToEndPointDistance());
9670     }
9671 
9672     private Integer getWallAngleInDegrees(Wall wall, float startPointToEndPointDistance) {
9673       Wall wallAtStart = wall.getWallAtStart();
9674       if (wallAtStart != null) {
9675         float wallAtStartSegmentDistance = wallAtStart.getStartPointToEndPointDistance();
9676         if (startPointToEndPointDistance != 0 && wallAtStartSegmentDistance != 0) {
9677           // Compute the angle between the wall and its wall at start
9678           float xWallVector = (wall.getXEnd() - wall.getXStart()) / startPointToEndPointDistance;
9679           float yWallVector = (wall.getYEnd() - wall.getYStart()) / startPointToEndPointDistance;
9680           float xWallAtStartVector = (wallAtStart.getXEnd() - wallAtStart.getXStart()) / wallAtStartSegmentDistance;
9681           float yWallAtStartVector = (wallAtStart.getYEnd() - wallAtStart.getYStart()) / wallAtStartSegmentDistance;
9682           if (wallAtStart.getWallAtStart() == wall) {
9683             // Reverse wall at start direction
9684             xWallAtStartVector = -xWallAtStartVector;
9685             yWallAtStartVector = -yWallAtStartVector;
9686           }
9687           int wallAngle = (int)Math.round(180 - Math.toDegrees(Math.atan2(
9688               yWallVector * xWallAtStartVector - xWallVector * yWallAtStartVector,
9689               xWallVector * xWallAtStartVector + yWallVector * yWallAtStartVector)));
9690           if (wallAngle > 180) {
9691             wallAngle -= 360;
9692           }
9693           return wallAngle;
9694         }
9695       }
9696       if (startPointToEndPointDistance == 0) {
9697         return 0;
9698       } else {
9699         return (int)Math.round(Math.toDegrees(Math.atan2(
9700             wall.getYStart() - wall.getYEnd(), wall.getXEnd() - wall.getXStart())));
9701       }
9702     }
9703 
9704     /**
9705      * Returns arc extent from the circumscribed circle of the triangle
9706      * with vertices (x1, y1) (x2, y2) (x, y).
9707      */
9708     protected float getArcExtent(float x1, float y1, float x2, float y2, float x, float y) {
9709       float [] arcCenter = getCircumscribedCircleCenter(x1, y1, x2, y2, x, y);
9710       double startPointToBissectorLine1Distance = Point2D.distance(x1, y1, x2, y2) / 2;
9711       double arcCenterToWallDistance = Float.isInfinite(arcCenter [0]) || Float.isInfinite(arcCenter [1])
9712           ? Float.POSITIVE_INFINITY
9713           : Line2D.ptLineDist(x1, y1, x2, y2, arcCenter [0], arcCenter [1]);
9714       int mousePosition = Line2D.relativeCCW(x1, y1, x2, y2, x, y);
9715       int centerPosition = Line2D.relativeCCW(x1, y1, x2, y2, arcCenter [0], arcCenter [1]);
9716       float arcExtent;
9717       if (centerPosition == mousePosition) {
9718         arcExtent = (float)(Math.PI + 2 * Math.atan2(arcCenterToWallDistance, startPointToBissectorLine1Distance));
9719       } else {
9720         arcExtent = (float)(2 * Math.atan2(startPointToBissectorLine1Distance, arcCenterToWallDistance));
9721       }
9722       arcExtent = Math.min(arcExtent, 3 * (float)Math.PI / 2);
9723       arcExtent *= mousePosition;
9724       return arcExtent;
9725     }
9726 
9727     /**
9728      * Returns the circumscribed circle of the triangle with vertices (x1, y1) (x2, y2) (x, y).
9729      */
9730     private float [] getCircumscribedCircleCenter(float x1, float y1, float x2, float y2, float x, float y) {
9731       float [][] bissectorLine1 = getBissectorLine(x1, y1, x2, y2);
9732       float [][] bissectorLine2 = getBissectorLine(x1, y1, x, y);
9733       float [] arcCenter = computeIntersection(bissectorLine1 [0], bissectorLine1 [1],
9734           bissectorLine2 [0], bissectorLine2 [1]);
9735       return arcCenter;
9736     }
9737 
9738     private float [][] getBissectorLine(float x1, float y1, float x2, float y2) {
9739       float xMiddlePoint = (x1 + x2) / 2;
9740       float yMiddlePoint = (y1 + y2) / 2;
9741       float bissectorLineAlpha = (x1 - x2) / (y2 - y1);
9742       if (bissectorLineAlpha > 1E10) {
9743         // Vertical line
9744         return new float [][] {{xMiddlePoint, yMiddlePoint}, {xMiddlePoint, yMiddlePoint + 1}};
9745       } else {
9746         return new float [][] {{xMiddlePoint, yMiddlePoint}, {xMiddlePoint + 1, bissectorLineAlpha + yMiddlePoint}};
9747       }
9748     }
9749 
9750     protected void showWallAngleFeedback(Wall wall, boolean ignoreArcExtent) {
9751       Float arcExtent = wall.getArcExtent();
9752       if (!ignoreArcExtent && arcExtent != null) {
9753         if (arcExtent < 0) {
9754           getView().setAngleFeedback(wall.getXArcCircleCenter(), wall.getYArcCircleCenter(),
9755               wall.getXStart(), wall.getYStart(), wall.getXEnd(), wall.getYEnd());
9756         } else {
9757           getView().setAngleFeedback(wall.getXArcCircleCenter(), wall.getYArcCircleCenter(),
9758               wall.getXEnd(), wall.getYEnd(), wall.getXStart(), wall.getYStart());
9759         }
9760       } else if (this.wallAngleToolTipFeedback != null
9761                  && this.wallAngleToolTipFeedback.length() > 0) {
9762         Wall wallAtStart = wall.getWallAtStart();
9763         if (wallAtStart != null) {
9764           if (wallAtStart.getWallAtStart() == wall) {
9765             if (getWallAngleInDegrees(wall) > 0) {
9766               getView().setAngleFeedback(wall.getXStart(), wall.getYStart(),
9767                   wallAtStart.getXEnd(), wallAtStart.getYEnd(), wall.getXEnd(), wall.getYEnd());
9768             } else {
9769               getView().setAngleFeedback(wall.getXStart(), wall.getYStart(),
9770                   wall.getXEnd(), wall.getYEnd(), wallAtStart.getXEnd(), wallAtStart.getYEnd());
9771             }
9772           } else {
9773             if (getWallAngleInDegrees(wall) > 0) {
9774               getView().setAngleFeedback(wall.getXStart(), wall.getYStart(),
9775                   wallAtStart.getXStart(), wallAtStart.getYStart(),
9776                   wall.getXEnd(), wall.getYEnd());
9777             } else {
9778               getView().setAngleFeedback(wall.getXStart(), wall.getYStart(),
9779                   wall.getXEnd(), wall.getYEnd(),
9780                   wallAtStart.getXStart(), wallAtStart.getYStart());
9781             }
9782           }
9783         }
9784       }
9785     }
9786   }
9787 
9788   /**
9789    * Wall drawing state. This state manages wall creation at each mouse press.
9790    */
9791   private class WallDrawingState extends AbstractWallState {
9792     private float            xStart;
9793     private float            yStart;
9794     private float            xLastEnd;
9795     private float            yLastEnd;
9796     private Wall             wallStartAtStart;
9797     private Wall             wallEndAtStart;
9798     private Wall             newWall;
9799     private Wall             wallStartAtEnd;
9800     private Wall             wallEndAtEnd;
9801     private Wall             lastWall;
9802     private List<Selectable> oldSelection;
9803     private boolean          oldBasePlanLocked;
9804     private boolean          oldAllLevelsSelection;
9805     private List<Wall>       newWalls;
9806     private boolean          magnetismEnabled;
9807     private boolean          alignmentActivated;
9808     private boolean          roundWall;
9809     private long             lastWallCreationTime;
9810     private Float            wallArcExtent;
9811 
9812     @Override
9813     public Mode getMode() {
9814       return Mode.WALL_CREATION;
9815     }
9816 
9817     @Override
9818     public boolean isModificationState() {
9819       return true;
9820     }
9821 
9822     @Override
9823     public boolean isBasePlanModificationState() {
9824       return true;
9825     }
9826 
9827     @Override
9828     public void setMode(Mode mode) {
9829       // Escape current creation and change state to matching mode
9830       escape();
9831       if (mode == Mode.SELECTION) {
9832         setState(getSelectionState());
9833       } else if (mode == Mode.PANNING) {
9834         setState(getPanningState());
9835       } else if (mode == Mode.ROOM_CREATION) {
9836         setState(getRoomCreationState());
9837       } else if (mode == Mode.POLYLINE_CREATION) {
9838         setState(getPolylineCreationState());
9839       } else if (mode == Mode.DIMENSION_LINE_CREATION) {
9840         setState(getDimensionLineCreationState());
9841       } else if (mode == Mode.LABEL_CREATION) {
9842         setState(getLabelCreationState());
9843       }
9844     }
9845 
9846     @Override
9847     public void enter() {
9848       super.enter();
9849       this.oldSelection = home.getSelectedItems();
9850       this.oldBasePlanLocked = home.isBasePlanLocked();
9851       this.oldAllLevelsSelection = home.isAllLevelsSelection();
9852       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
9853       toggleMagnetism(wasMagnetismToggledLastMousePress());
9854       this.xStart = getXLastMouseMove();
9855       this.yStart = getYLastMouseMove();
9856       // If the start or end line of a wall close to (xStart, yStart) is
9857       // free, it will the wall at start of the new wall
9858       this.wallEndAtStart = getWallEndAt(this.xStart, this.yStart, null);
9859       if (this.wallEndAtStart != null) {
9860         this.wallStartAtStart = null;
9861         this.xStart = this.wallEndAtStart.getXEnd();
9862         this.yStart = this.wallEndAtStart.getYEnd();
9863       } else {
9864         this.wallStartAtStart = getWallStartAt(
9865             this.xStart, this.yStart, null);
9866         if (this.wallStartAtStart != null) {
9867           this.xStart = this.wallStartAtStart.getXStart();
9868           this.yStart = this.wallStartAtStart.getYStart();
9869         } else if (this.magnetismEnabled) {
9870           WallPointWithAngleMagnetism point = new WallPointWithAngleMagnetism(null, this.xStart, this.yStart, this.xStart, this.yStart);
9871           this.xStart = point.getX();
9872           this.yStart = point.getY();
9873         }
9874       }
9875       this.newWall = null;
9876       this.wallStartAtEnd = null;
9877       this.wallEndAtEnd = null;
9878       this.lastWall = null;
9879       this.newWalls = new ArrayList<Wall>();
9880       this.lastWallCreationTime = -1;
9881       deselectAll();
9882       setDuplicationActivated(wasDuplicationActivatedLastMousePress());
9883       PlanView planView = getView();
9884       planView.setAlignmentFeedback(Wall.class, null, this.xStart, this.yStart, false);
9885     }
9886 
9887     @Override
9888     public void moveMouse(float x, float y) {
9889       PlanView planView = getView();
9890       // Compute the coordinates where wall end point should be moved
9891       float xEnd;
9892       float yEnd;
9893       if (this.alignmentActivated) {
9894         PointWithAngleMagnetism point = new PointWithAngleMagnetism(this.xStart, this.yStart, x, y,
9895             preferences.getLengthUnit(), planView.getPixelLength());
9896         xEnd = point.getX();
9897         yEnd = point.getY();
9898       } else if (this.magnetismEnabled) {
9899         WallPointWithAngleMagnetism point = new WallPointWithAngleMagnetism(this.newWall, this.xStart, this.yStart, x, y);
9900         xEnd = point.getX();
9901         yEnd = point.getY();
9902       } else {
9903         xEnd = x;
9904         yEnd = y;
9905       }
9906 
9907       // If current wall doesn't exist
9908       if (this.newWall == null) {
9909         // Create a new one
9910         this.newWall = createWall(this.xStart, this.yStart,
9911             xEnd, yEnd, this.wallStartAtStart, this.wallEndAtStart);
9912         this.newWalls.add(this.newWall);
9913       } else if (this.wallArcExtent != null) {
9914         // Compute current wall arc extent from the circumscribed circle of the triangle
9915         // with vertices (xStart, yStart) (xEnd, yEnd) (x, y)
9916         this.wallArcExtent = getArcExtent(this.newWall.getXStart(), this.newWall.getXEnd(),
9917             this.newWall.getYStart(), this.newWall.getYEnd(), x, y);
9918         if (this.alignmentActivated
9919             || this.magnetismEnabled) {
9920           this.wallArcExtent = (float)Math.toRadians(Math.round(Math.toDegrees(this.wallArcExtent)));
9921         }
9922         this.newWall.setArcExtent(this.wallArcExtent);
9923       } else {
9924         // Otherwise update its end point
9925         this.newWall.setXEnd(xEnd);
9926         this.newWall.setYEnd(yEnd);
9927       }
9928       planView.setToolTipFeedback(getToolTipFeedbackText(this.newWall, false), x, y);
9929       planView.setAlignmentFeedback(Wall.class, this.newWall, xEnd, yEnd, false);
9930       showWallAngleFeedback(this.newWall, false);
9931 
9932       // If the start or end line of a wall close to (xEnd, yEnd) is
9933       // free, it will the wall at end of the new wall.
9934       this.wallStartAtEnd = getWallStartAt(xEnd, yEnd, this.newWall);
9935       if (this.wallStartAtEnd != null) {
9936         this.wallEndAtEnd = null;
9937         // Select the wall with a free start to display a feedback to user
9938         selectItem(this.wallStartAtEnd);
9939       } else {
9940         this.wallEndAtEnd = getWallEndAt(xEnd, yEnd, this.newWall);
9941         if (this.wallEndAtEnd != null) {
9942           // Select the wall with a free end to display a feedback to user
9943           selectItem(this.wallEndAtEnd);
9944         } else {
9945           deselectAll();
9946         }
9947       }
9948 
9949       // Ensure point at (x,y) is visible
9950       planView.makePointVisible(x, y);
9951       // Update move coordinates
9952       this.xLastEnd = xEnd;
9953       this.yLastEnd = yEnd;
9954     }
9955 
9956     @Override
9957     public void pressMouse(float x, float y, int clickCount,
9958                            boolean shiftDown, boolean duplicationActivated) {
9959       if (clickCount == 2) {
9960         Selectable selectableItem = getSelectableItemAt(x, y);
9961         if (this.newWalls.size() == 0
9962             && selectableItem instanceof Room) {
9963           createWallsAroundRoom((Room)selectableItem);
9964         } else {
9965           if (this.roundWall && this.newWall != null) {
9966             // Let's end wall creation of round walls after a double click
9967             endWallCreation();
9968           }
9969           if (this.lastWall != null) {
9970             // Join last wall to the selected wall at its end
9971             joinNewWallEndToWall(this.lastWall,
9972                 this.wallStartAtEnd, this.wallEndAtEnd);
9973           }
9974         }
9975         validateDrawnWalls();
9976       } else {
9977         // Create a new wall only when it will have a distance between start and end points > 0
9978         if (this.newWall != null
9979             && this.newWall.getStartPointToEndPointDistance() > 0) {
9980           if (this.roundWall && this.wallArcExtent == null) {
9981             this.wallArcExtent = (float)Math.PI;
9982             this.newWall.setArcExtent(this.wallArcExtent);
9983             getView().setToolTipFeedback(getToolTipFeedbackText(this.newWall, false), x, y);
9984           } else {
9985             getView().deleteToolTipFeedback();
9986             selectItem(this.newWall);
9987             endWallCreation();
9988           }
9989         }
9990       }
9991     }
9992 
9993     /**
9994      * Creates walls around the given <code>room</code>.
9995      */
9996     private void createWallsAroundRoom(Room room) {
9997       if (room.isSingular()) {
9998         float [][] roomPoints = room.getPoints();
9999         List<float []> pointsList = new ArrayList<float[]>(Arrays.asList(roomPoints));
10000         // It points are not clockwise reverse their order
10001         if (!room.isClockwise()) {
10002           Collections.reverse(pointsList);
10003         }
10004         // Remove equal points
10005         for (int i = 0; i < pointsList.size(); ) {
10006           float [] point = pointsList.get(i);
10007           float [] nextPoint = pointsList.get((i + 1) % pointsList.size());
10008           if (point [0] == nextPoint [0]
10009               && point [1] == nextPoint [1]) {
10010             pointsList.remove(i);
10011           } else {
10012             i++;
10013           }
10014         }
10015         roomPoints = pointsList.toArray(new float [pointsList.size()][]);
10016 
10017         float halfWallThickness = preferences.getNewWallThickness() / 2;
10018         float [][] largerRoomPoints = new float [roomPoints.length][];
10019         for (int i = 0; i < roomPoints.length; i++) {
10020           float [] point = roomPoints [i];
10021           float [] previousPoint = roomPoints [(i + roomPoints.length - 1) % roomPoints.length];
10022           float [] nextPoint     = roomPoints [(i + 1) % roomPoints.length];
10023 
10024           // Compute the angle of the line with a direction orthogonal to line (previousPoint, point)
10025           double previousAngle = Math.atan2(point [0] - previousPoint [0], previousPoint [1] - point [1]);
10026           // Compute the points of the line joining previous and current point
10027           // at a distance equal to the half wall thickness
10028           float deltaX = (float)(Math.cos(previousAngle) * halfWallThickness);
10029           float deltaY = (float)(Math.sin(previousAngle) * halfWallThickness);
10030           float [] point1 = {previousPoint [0] - deltaX, previousPoint [1] - deltaY};
10031           float [] point2 = {point [0] - deltaX, point [1] - deltaY};
10032 
10033           // Compute the angle of the line with a direction orthogonal to line (point, nextPoint)
10034           double nextAngle = Math.atan2(nextPoint [0] - point [0], point [1] - nextPoint [1]);
10035           // Compute the points of the line joining current and next point
10036           // at a distance equal to the half wall thickness
10037           deltaX = (float)(Math.cos(nextAngle) * halfWallThickness);
10038           deltaY = (float)(Math.sin(nextAngle) * halfWallThickness);
10039           float [] point3 = {point [0] - deltaX, point [1] - deltaY};
10040           float [] point4 = {nextPoint [0] - deltaX, nextPoint [1] - deltaY};
10041 
10042           largerRoomPoints [i] = computeIntersection(point1, point2, point3, point4);
10043         }
10044 
10045         // Create walls joining points of largerRoomPoints
10046         Wall lastWall = null;
10047         Area wallsArea = getWallsArea(false);
10048         float thinThickness = 0.05f;
10049         for (int i = 0; i < largerRoomPoints.length; i++) {
10050           final float [] sidePoint = largerRoomPoints [i];
10051           float [] nextSidePoint   = largerRoomPoints [(i + 1) % roomPoints.length];
10052           // Remove from wall line the intersection with existing walls
10053           Area lineArea = new Area(getPath(new Wall(
10054               sidePoint [0], sidePoint [1], nextSidePoint [0], nextSidePoint [1], thinThickness, 0).getPoints()));
10055           lineArea.subtract(wallsArea);
10056           List<GeneralPath> newWallPaths = getAreaPaths(lineArea);
10057           List<Wall> roomSideWalls = new ArrayList<Wall>();
10058           int ignoredWall = 0;
10059           for (int j = 0; j < newWallPaths.size(); j++) {
10060             float [][] newWallPoints = getPathPoints(newWallPaths.get(j), false);
10061             if (newWallPoints.length > 4) {
10062               // Try to remove aligned points
10063               newWallPoints = getPathPoints(newWallPaths.get(j), true);
10064             }
10065             if (newWallPoints.length == 4) {
10066               // Search the two ends of the line
10067               float [] point1;
10068               float [] point2;
10069               if (Point2D.distanceSq(newWallPoints [0][0], newWallPoints [0][1], newWallPoints [1][0], newWallPoints [1][1])
10070                   < Point2D.distanceSq(newWallPoints [0][0], newWallPoints [0][1], newWallPoints [3][0], newWallPoints [3][1])) {
10071                 point1 = new float [] {(newWallPoints [0][0] + newWallPoints [1][0]) / 2, (newWallPoints [0][1] + newWallPoints [1][1]) / 2};
10072                 point2 = new float [] {(newWallPoints [2][0] + newWallPoints [3][0]) / 2, (newWallPoints [2][1] + newWallPoints [3][1]) / 2};
10073               } else {
10074                 point1 = new float [] {(newWallPoints [0][0] + newWallPoints [3][0]) / 2, (newWallPoints [0][1] + newWallPoints [3][1]) / 2};
10075                 point2 = new float [] {(newWallPoints [1][0] + newWallPoints [2][0]) / 2, (newWallPoints [1][1] + newWallPoints [2][1]) / 2};
10076               }
10077               float [] startPoint;
10078               float [] endPoint;
10079               if (Point2D.distanceSq(point1 [0], point1 [1], sidePoint [0], sidePoint [1])
10080                   < Point2D.distanceSq(point2 [0], point2 [1], sidePoint [0], sidePoint [1])) {
10081                 startPoint = point1;
10082                 endPoint = point2;
10083               } else {
10084                 startPoint = point2;
10085                 endPoint = point1;
10086               }
10087               if (Point2D.distanceSq(startPoint [0], startPoint [1], endPoint [0], endPoint [1]) > 0.01) {
10088                 roomSideWalls.add(createWall(startPoint [0], startPoint [1], endPoint [0], endPoint [1], null,
10089                     lastWall != null && Point2D.distanceSq(lastWall.getXEnd(), lastWall.getYEnd(), startPoint [0], startPoint [1]) < 1E-2 ? lastWall : null));
10090               } else {
10091                 ignoredWall++;
10092               }
10093             }
10094           }
10095           if (newWallPaths.size() > ignoredWall && roomSideWalls.isEmpty()) {
10096             Wall existingWall = null;
10097             for (Wall wall : home.getWalls()) {
10098               if (wall.isAtLevel(home.getSelectedLevel())
10099                   && Math.abs(wall.getXStart() - sidePoint [0]) < 0.05
10100                   && Math.abs(wall.getYStart() - sidePoint [1]) < 0.05
10101                   && Math.abs(wall.getXEnd() - nextSidePoint [0]) < 0.05
10102                   && Math.abs(wall.getYEnd() - nextSidePoint [1]) < 0.05
10103                   && (wall.getArcExtent() == null
10104                      || wall.getArcExtent() == 0)) {
10105                 existingWall = wall;
10106                 break;
10107               }
10108             }
10109             if (existingWall == null) {
10110               // Ensure at least a wall is created for each room side that doesn't exist
10111               roomSideWalls.add(createWall(sidePoint [0], sidePoint [1], nextSidePoint [0], nextSidePoint [1], null, lastWall));
10112             }
10113           }
10114           if (roomSideWalls.size() > 0) {
10115             // Order walls from the closest to first point to the farthest one
10116             Collections.sort(roomSideWalls, new Comparator<Wall>() {
10117                 public int compare(Wall wall1, Wall wall2) {
10118                   return Double.compare(Point2D.distanceSq(wall1.getXStart(), wall1.getYStart(), sidePoint [0], sidePoint [1]),
10119                       Point2D.distanceSq(wall2.getXStart(), wall2.getYStart(), sidePoint [0], sidePoint [1]));
10120                 }
10121               });
10122             this.newWalls.addAll(roomSideWalls);
10123             lastWall = roomSideWalls.get(roomSideWalls.size() - 1);
10124           } else {
10125             lastWall = null;
10126           }
10127         }
10128         if (lastWall != null && Point2D.distanceSq(lastWall.getXEnd(), lastWall.getYEnd(), this.newWalls.get(0).getXStart(), this.newWalls.get(0).getYStart()) < 1E-2) {
10129           joinNewWallEndToWall(lastWall, this.newWalls.get(0), null);
10130         }
10131       }
10132     }
10133 
10134     private void validateDrawnWalls() {
10135       if (this.newWalls.size() > 0) {
10136         // Post walls creation to undo support
10137         postCreateWalls(this.newWalls, this.oldSelection, this.oldBasePlanLocked, this.oldAllLevelsSelection);
10138         selectItems(this.newWalls);
10139       }
10140       if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
10141         setState(getSelectionState());
10142       } else {
10143         // Change state to WallCreationState
10144         setState(getWallCreationState());
10145       }
10146     }
10147 
10148     private void endWallCreation() {
10149       this.lastWall =
10150       this.wallEndAtStart = this.newWall;
10151       this.wallStartAtStart = null;
10152       this.xStart = this.newWall.getXEnd();
10153       this.yStart = this.newWall.getYEnd();
10154       this.newWall = null;
10155       this.wallArcExtent = null;
10156     }
10157 
10158     @Override
10159     public void setEditionActivated(boolean editionActivated) {
10160       PlanView planView = getView();
10161       if (editionActivated) {
10162         planView.deleteFeedback();
10163         if (this.newWalls.size() == 0
10164             && this.wallEndAtStart == null
10165             && this.wallStartAtStart == null) {
10166           // Edit xStart and yStart
10167           planView.setToolTipEditedProperties(new EditableProperty [] {EditableProperty.X,
10168                                                                        EditableProperty.Y},
10169               new Object [] {this.xStart, this.yStart},
10170               this.xStart, this.yStart);
10171         } else {
10172           if (this.newWall == null) {
10173             // May happen if edition is activated after the user clicked to finish one wall
10174             createNextWall();
10175           }
10176           if (this.wallArcExtent == null) {
10177             // Edit length, angle and thickness
10178             planView.setToolTipEditedProperties(new EditableProperty [] {EditableProperty.LENGTH,
10179                                                                          EditableProperty.ANGLE,
10180                                                                          EditableProperty.THICKNESS},
10181                 new Object [] {this.newWall.getLength(),
10182                                getWallAngleInDegrees(this.newWall),
10183                                this.newWall.getThickness()},
10184                 this.newWall.getXEnd(), this.newWall.getYEnd());
10185           } else {
10186             // Edit arc extent
10187             planView.setToolTipEditedProperties(new EditableProperty [] {EditableProperty.ARC_EXTENT},
10188                 new Object [] {new Integer((int)Math.round(Math.toDegrees(this.wallArcExtent)))},
10189                 this.newWall.getXEnd(), this.newWall.getYEnd());
10190           }
10191         }
10192       } else {
10193         if (this.newWall == null) {
10194           // Create a new wall once user entered the start point of the first wall
10195           LengthUnit lengthUnit = preferences.getLengthUnit();
10196           float defaultLength = lengthUnit == LengthUnit.INCH || lengthUnit == LengthUnit.INCH_DECIMALS
10197               ? LengthUnit.footToCentimeter(10) : 300;
10198           this.xLastEnd = this.xStart + defaultLength;
10199           this.yLastEnd = this.yStart;
10200           this.newWall = createWall(this.xStart, this.yStart,
10201               this.xLastEnd, this.yLastEnd, this.wallStartAtStart, this.wallEndAtStart);
10202           this.newWalls.add(this.newWall);
10203           // Activate automatically second step to let user enter the
10204           // length, angle and thickness of the new wall
10205           planView.deleteFeedback();
10206           setEditionActivated(true);
10207         } else if (this.roundWall && this.wallArcExtent == null) {
10208           this.wallArcExtent = (float)Math.PI;
10209           this.newWall.setArcExtent(this.wallArcExtent);
10210           setEditionActivated(true);
10211         } else if (System.currentTimeMillis() - this.lastWallCreationTime < 300) {
10212           // If the user deactivated edition less than 300 ms after activation,
10213           // validate drawn walls after removing the last added wall
10214           if (this.newWalls.size() > 1) {
10215             this.newWalls.remove(this.newWall);
10216             home.deleteWall(this.newWall);
10217           }
10218           validateDrawnWalls();
10219         } else {
10220           endWallCreation();
10221           if (this.newWalls.size() > 2 && this.wallStartAtEnd != null) {
10222             // Join last wall to the first wall at its end and validate drawn walls
10223             joinNewWallEndToWall(this.lastWall, this.wallStartAtEnd, null);
10224             validateDrawnWalls();
10225             return;
10226           }
10227           createNextWall();
10228           // Reactivate automatically second step
10229           planView.deleteToolTipFeedback();
10230           setEditionActivated(true);
10231         }
10232       }
10233     }
10234 
10235     private void createNextWall() {
10236       Wall previousWall = this.wallEndAtStart != null
10237           ? this.wallEndAtStart
10238           : this.wallStartAtStart;
10239       // Create a new wall with an angle equal to previous wall angle - 90�
10240       double previousWallAngle = Math.PI - Math.atan2(previousWall.getYStart() - previousWall.getYEnd(),
10241           previousWall.getXStart() - previousWall.getXEnd());
10242       previousWallAngle -=  Math.PI / 2;
10243       float previousWallSegmentDistance = previousWall.getStartPointToEndPointDistance();
10244       this.xLastEnd = (float)(this.xStart + previousWallSegmentDistance * Math.cos(previousWallAngle));
10245       this.yLastEnd = (float)(this.yStart - previousWallSegmentDistance * Math.sin(previousWallAngle));
10246       this.newWall = createWall(this.xStart, this.yStart,
10247           this.xLastEnd, this.yLastEnd, this.wallStartAtStart, previousWall);
10248       this.newWall.setThickness(previousWall.getThickness());
10249       this.newWalls.add(this.newWall);
10250       this.lastWallCreationTime = System.currentTimeMillis();
10251       deselectAll();
10252     }
10253 
10254     @Override
10255     public void updateEditableProperty(EditableProperty editableProperty, Object value) {
10256       PlanView planView = getView();
10257       if (this.newWall == null) {
10258         float maximumLength = preferences.getLengthUnit().getMaximumLength();
10259         // Update start point of the first wall
10260         switch (editableProperty) {
10261           case X :
10262             this.xStart = value != null ? ((Number)value).floatValue() : 0;
10263             this.xStart = Math.max(-maximumLength, Math.min(this.xStart, maximumLength));
10264             break;
10265           case Y :
10266             this.yStart = value != null ? ((Number)value).floatValue() : 0;
10267             this.yStart = Math.max(-maximumLength, Math.min(this.yStart, maximumLength));
10268             break;
10269         }
10270         planView.setAlignmentFeedback(Wall.class, null, this.xStart, this.yStart, true);
10271         planView.makePointVisible(this.xStart, this.yStart);
10272       } else {
10273         if (editableProperty == EditableProperty.THICKNESS) {
10274           float thickness = value != null ? Math.abs(((Number)value).floatValue()) : 0;
10275           thickness = Math.max(0.01f, Math.min(thickness, 1000));
10276           this.newWall.setThickness(thickness);
10277         } else if (editableProperty == EditableProperty.ARC_EXTENT) {
10278           double arcExtent = Math.toRadians(value != null ? ((Number)value).doubleValue() : 0);
10279           this.wallArcExtent = (float)(Math.signum(arcExtent) * Math.min(Math.abs(arcExtent), 3 * Math.PI / 2));
10280           this.newWall.setArcExtent(this.wallArcExtent);
10281           showWallAngleFeedback(this.newWall, false);
10282         } else {
10283           // Update end point of the current wall
10284           switch (editableProperty) {
10285             case LENGTH :
10286               float length = value != null ? ((Number)value).floatValue() : 0;
10287               length = Math.max(0.001f, Math.min(length, preferences.getLengthUnit().getMaximumLength()));
10288               double wallAngle = Math.PI - Math.atan2(this.yStart - this.yLastEnd, this.xStart - this.xLastEnd);
10289               this.xLastEnd = (float)(this.xStart + length * Math.cos(wallAngle));
10290               this.yLastEnd = (float)(this.yStart - length * Math.sin(wallAngle));
10291               break;
10292             case ANGLE :
10293               wallAngle = Math.toRadians(value != null ? ((Number)value).doubleValue() : 0);
10294               Wall previousWall = this.newWall.getWallAtStart();
10295               if (previousWall != null
10296                   && previousWall.getStartPointToEndPointDistance() > 0) {
10297                 wallAngle -= Math.atan2(previousWall.getYStart() - previousWall.getYEnd(),
10298                     previousWall.getXStart() - previousWall.getXEnd());
10299               }
10300               float startPointToEndPointDistance = this.newWall.getStartPointToEndPointDistance();
10301               this.xLastEnd = (float)(this.xStart + startPointToEndPointDistance * Math.cos(wallAngle));
10302               this.yLastEnd = (float)(this.yStart - startPointToEndPointDistance * Math.sin(wallAngle));
10303               break;
10304             default :
10305               return;
10306           }
10307 
10308           // Update new wall
10309           this.newWall.setXEnd(this.xLastEnd);
10310           this.newWall.setYEnd(this.yLastEnd);
10311           planView.setAlignmentFeedback(Wall.class, this.newWall, this.xLastEnd, this.yLastEnd, false);
10312           showWallAngleFeedback(this.newWall, false);
10313           // Ensure wall points are visible
10314           planView.makePointVisible(this.xStart, this.yStart);
10315           planView.makePointVisible(this.xLastEnd, this.yLastEnd);
10316           // Search if the free start point of the first wall matches the end point of the current wall
10317           if (this.newWalls.size() > 2
10318               && this.newWalls.get(0).getWallAtStart() == null
10319               && this.newWalls.get(0).containsWallStartAt(this.xLastEnd, this.yLastEnd, 1E-3f)) {
10320             this.wallStartAtEnd = this.newWalls.get(0);
10321             selectItem(this.wallStartAtEnd);
10322           } else {
10323             this.wallStartAtEnd = null;
10324             deselectAll();
10325           }
10326         }
10327       }
10328     }
10329 
10330     @Override
10331     public void toggleMagnetism(boolean magnetismToggled) {
10332       // Compute active magnetism
10333       this.magnetismEnabled = preferences.isMagnetismEnabled()
10334                               ^ magnetismToggled;
10335       // If the new wall already exists,
10336       // compute again its end as if mouse moved
10337       if (this.newWall != null) {
10338         moveMouse(getXLastMouseMove(), getYLastMouseMove());
10339       }
10340     }
10341 
10342     @Override
10343     public void setAlignmentActivated(boolean alignmentActivated) {
10344       this.alignmentActivated = alignmentActivated;
10345       moveMouse(getXLastMouseMove(), getYLastMouseMove());
10346     }
10347 
10348     @Override
10349     public void setDuplicationActivated(boolean duplicationActivated) {
10350       // Reuse duplication activation for round circle creation
10351       this.roundWall = duplicationActivated;
10352     }
10353 
10354     @Override
10355     public void escape() {
10356       if (this.newWall != null) {
10357         // Delete current created wall
10358         home.deleteWall(this.newWall);
10359         this.newWalls.remove(this.newWall);
10360       }
10361       validateDrawnWalls();
10362     }
10363 
10364     @Override
10365     public void exit() {
10366       PlanView planView = getView();
10367       planView.deleteFeedback();
10368       this.wallStartAtStart = null;
10369       this.wallEndAtStart = null;
10370       this.newWall = null;
10371       this.wallArcExtent = null;
10372       this.wallStartAtEnd = null;
10373       this.wallEndAtEnd = null;
10374       this.lastWall = null;
10375       this.oldSelection = null;
10376       this.newWalls = null;
10377     }
10378   }
10379 
10380   /**
10381    * Wall resize state. This state manages wall resizing.
10382    */
10383   private class WallResizeState extends AbstractWallState {
10384     private Wall         selectedWall;
10385     private boolean      startPoint;
10386     private float        oldX;
10387     private float        oldY;
10388     private float        deltaXToResizePoint;
10389     private float        deltaYToResizePoint;
10390     private boolean      magnetismEnabled;
10391     private boolean      alignmentActivated;
10392 
10393     @Override
10394     public Mode getMode() {
10395       return Mode.SELECTION;
10396     }
10397 
10398     @Override
10399     public boolean isModificationState() {
10400       return true;
10401     }
10402 
10403     @Override
10404     public boolean isBasePlanModificationState() {
10405       return true;
10406     }
10407 
10408     @Override
10409     public void enter() {
10410       super.enter();
10411       this.selectedWall = (Wall)home.getSelectedItems().get(0);
10412       this.startPoint = this.selectedWall
10413           == getResizedWallStartAt(getXLastMousePress(), getYLastMousePress());
10414       if (this.startPoint) {
10415         this.oldX = this.selectedWall.getXStart();
10416         this.oldY = this.selectedWall.getYStart();
10417       } else {
10418         this.oldX = this.selectedWall.getXEnd();
10419         this.oldY = this.selectedWall.getYEnd();
10420       }
10421       this.deltaXToResizePoint = getXLastMousePress() - this.oldX;
10422       this.deltaYToResizePoint = getYLastMousePress() - this.oldY;
10423       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
10424       toggleMagnetism(wasMagnetismToggledLastMousePress());
10425       PlanView planView = getView();
10426       planView.setResizeIndicatorVisible(true);
10427       planView.setToolTipFeedback(getToolTipFeedbackText(this.selectedWall, true),
10428           getXLastMousePress(), getYLastMousePress());
10429       planView.setAlignmentFeedback(Wall.class, this.selectedWall, this.oldX, this.oldY, false);
10430       showWallAngleFeedback(this.selectedWall, true);
10431       planView.setDimensionLinesFeedback(getDimensionLinesAlongWall(this.selectedWall));
10432     }
10433 
10434     @Override
10435     public void moveMouse(float x, float y) {
10436       PlanView planView = getView();
10437       float newX = x - this.deltaXToResizePoint;
10438       float newY = y - this.deltaYToResizePoint;
10439       float opositeEndX = this.startPoint
10440           ? this.selectedWall.getXEnd()
10441           : this.selectedWall.getXStart();
10442       float opositeEndY = this.startPoint
10443           ? this.selectedWall.getYEnd()
10444           : this.selectedWall.getYStart();
10445       if (this.alignmentActivated) {
10446         PointWithAngleMagnetism point = new PointWithAngleMagnetism(opositeEndX, opositeEndY, newX, newY,
10447             preferences.getLengthUnit(), planView.getPixelLength());
10448         newX = point.getX();
10449         newY = point.getY();
10450       } else if (this.magnetismEnabled) {
10451         WallPointWithAngleMagnetism point = new WallPointWithAngleMagnetism(this.selectedWall,
10452             opositeEndX, opositeEndY, newX, newY);
10453         newX = point.getX();
10454         newY = point.getY();
10455       }
10456       moveWallPoint(this.selectedWall, newX, newY, this.startPoint);
10457 
10458       planView.setToolTipFeedback(getToolTipFeedbackText(this.selectedWall, true), x, y);
10459       planView.setAlignmentFeedback(Wall.class, this.selectedWall, newX, newY, false);
10460       showWallAngleFeedback(this.selectedWall, true);
10461       planView.setDimensionLinesFeedback(getDimensionLinesAlongWall(this.selectedWall));
10462       // Ensure point at (x,y) is visible
10463       planView.makePointVisible(x, y);
10464     }
10465 
10466     private List<DimensionLine> getDimensionLinesAlongWall(Wall wall) {
10467       List<DimensionLine> dimensionLines = new ArrayList<DimensionLine>();
10468       if (wall.getArcExtent() == null || wall.getArcExtent() == 0) {
10469         float offset = 20 / getView().getScale();
10470         float [][] wallPoints = wall.getPoints();
10471         // Search among room paths which segment are included in wall sides
10472         List<GeneralPath> roomPaths = getRoomPathsFromWalls();
10473         for (int i = 0; i < roomPaths.size(); i++) {
10474           float [][] roomPoints = getPathPoints(roomPaths.get(i), true);
10475           for (int j = 0; j < roomPoints.length; j++) {
10476             float [] startPoint = roomPoints [j];
10477             float [] endPoint = roomPoints [(j + 1) % roomPoints.length];
10478             boolean segmentPartOfLeftSide = Line2D.ptLineDistSq(wallPoints [0][0], wallPoints [0][1], wallPoints [1][0], wallPoints [1][1], startPoint [0], startPoint [1]) < 0.0001
10479                 && Line2D.ptLineDistSq(wallPoints [0][0], wallPoints [0][1], wallPoints [1][0], wallPoints [1][1], endPoint [0], endPoint [1]) < 0.0001;
10480             boolean segmentAccepted;
10481             if (segmentPartOfLeftSide) {
10482               // Check that either end of [startPoint, endPoint] or the wall side belongs to one of the segment
10483               segmentAccepted = Line2D.ptSegDistSq(wallPoints [0][0], wallPoints [0][1], wallPoints [1][0], wallPoints [1][1], startPoint [0], startPoint [1]) < 0.0001
10484                   || Line2D.ptSegDistSq(wallPoints [0][0], wallPoints [0][1], wallPoints [1][0], wallPoints [1][1], endPoint [0], endPoint [1]) < 0.0001
10485                   || Line2D.ptSegDistSq(startPoint [0], startPoint [1], endPoint [0], endPoint [1], wallPoints [0][0], wallPoints [0][1]) < 0.0001
10486                   || Line2D.ptSegDistSq(startPoint [0], startPoint [1], endPoint [0], endPoint [1], wallPoints [1][0], wallPoints [1][1]) < 0.0001;
10487             } else {
10488               segmentAccepted = Line2D.ptLineDistSq(wallPoints [2][0], wallPoints [2][1], wallPoints [3][0], wallPoints [3][1], startPoint [0], startPoint [1]) < 0.0001
10489                   && Line2D.ptLineDistSq(wallPoints [2][0], wallPoints [2][1], wallPoints [3][0], wallPoints [3][1], endPoint [0], endPoint [1]) < 0.0001
10490                   && (Line2D.ptSegDistSq(wallPoints [2][0], wallPoints [2][1], wallPoints [3][0], wallPoints [3][1], startPoint [0], startPoint [1]) < 0.0001
10491                       || Line2D.ptSegDistSq(wallPoints [2][0], wallPoints [2][1], wallPoints [3][0], wallPoints [3][1], endPoint [0], endPoint [1]) < 0.0001
10492                       || Line2D.ptSegDistSq(startPoint [0], startPoint [1], endPoint [0], endPoint [1], wallPoints [2][0], wallPoints [2][1]) < 0.0001
10493                       || Line2D.ptSegDistSq(startPoint [0], startPoint [1], endPoint [0], endPoint [1], wallPoints [3][0], wallPoints [3][1]) < 0.0001);
10494             }
10495             if (segmentAccepted) {
10496               dimensionLines.add(getDimensionLineBetweenPoints(startPoint, endPoint,
10497                   isDimensionInsideWall(wall, startPoint, endPoint) ? -offset : offset, false));
10498             }
10499           }
10500         }
10501         for (int i = dimensionLines.size() - 1; i >= 0; i--) {
10502           if (dimensionLines.get(i).getLength() < 0.01f) {
10503             dimensionLines.remove(i);
10504           }
10505         }
10506         if (dimensionLines.size() == 2
10507             && Math.abs(dimensionLines.get(0).getLength() - dimensionLines.get(1).getLength()) < 0.01f) {
10508           dimensionLines.remove(1);
10509         }
10510       }
10511       return dimensionLines;
10512     }
10513 
10514     private boolean isDimensionInsideWall(Wall wall, float [] point1, float [] point2) {
10515       // Search the coordinates of the point where the dimension will be displayed
10516       AffineTransform rotation = AffineTransform.getRotateInstance(
10517           Math.atan2(point2 [1] - point1 [1], point2 [0] - point1 [0]), point1 [0], point1 [1]);
10518       float [] dimensionPoint = {(float)(point1 [0] + Point2D.distance(point1 [0], point1 [1], point2 [0], point2 [1]) / 2), point1 [1] + 0.01f};
10519       rotation.transform(dimensionPoint, 0, dimensionPoint, 0, 1);
10520       return wall.containsPoint(dimensionPoint [0], dimensionPoint [1], 0);
10521     }
10522 
10523     @Override
10524     public void releaseMouse(float x, float y) {
10525       postWallResize(this.selectedWall, this.oldX, this.oldY, this.startPoint);
10526       setState(getSelectionState());
10527     }
10528 
10529     @Override
10530     public void toggleMagnetism(boolean magnetismToggled) {
10531       // Compute active magnetism
10532       this.magnetismEnabled = preferences.isMagnetismEnabled()
10533                               ^ magnetismToggled;
10534       moveMouse(getXLastMouseMove(), getYLastMouseMove());
10535     }
10536 
10537     @Override
10538     public void setAlignmentActivated(boolean alignmentActivated) {
10539       this.alignmentActivated = alignmentActivated;
10540       moveMouse(getXLastMouseMove(), getYLastMouseMove());
10541     }
10542 
10543     @Override
10544     public void escape() {
10545       moveWallPoint(this.selectedWall, this.oldX, this.oldY, this.startPoint);
10546       setState(getSelectionState());
10547     }
10548 
10549     @Override
10550     public void exit() {
10551       PlanView planView = getView();
10552       planView.setResizeIndicatorVisible(false);
10553       planView.deleteFeedback();
10554       this.selectedWall = null;
10555     }
10556   }
10557 
10558   /**
10559    * Wall arc extent state. This state manages wall arc extent change.
10560    */
10561   private class WallArcExtentState extends AbstractWallState {
10562     private Wall         selectedWall;
10563     private Float        oldArcExtent;
10564     private float        deltaXToMiddlePoint;
10565     private float        deltaYToMiddlePoint;
10566     private boolean      magnetismEnabled;
10567     private boolean      alignmentActivated;
10568 
10569     @Override
10570     public Mode getMode() {
10571       return Mode.SELECTION;
10572     }
10573 
10574     @Override
10575     public boolean isModificationState() {
10576       return true;
10577     }
10578 
10579     @Override
10580     public boolean isBasePlanModificationState() {
10581       return true;
10582     }
10583 
10584     @Override
10585     public void enter() {
10586       super.enter();
10587       this.selectedWall = (Wall)home.getSelectedItems().get(0);
10588       this.oldArcExtent = this.selectedWall.getArcExtent();
10589       float [][] wallPoints = this.selectedWall.getPoints();
10590       int leftSideMiddlePointIndex = wallPoints.length / 4;
10591       int rightSideMiddlePointIndex = wallPoints.length - 1 - leftSideMiddlePointIndex;
10592       if (wallPoints.length % 4 == 0) {
10593         leftSideMiddlePointIndex--;
10594       }
10595       float middleX = (wallPoints [leftSideMiddlePointIndex][0] + wallPoints [rightSideMiddlePointIndex][0]) / 2;
10596       float middleY = (wallPoints [leftSideMiddlePointIndex][1] + wallPoints [rightSideMiddlePointIndex][1]) / 2;
10597       this.deltaXToMiddlePoint = getXLastMousePress() - middleX;
10598       this.deltaYToMiddlePoint = getYLastMousePress() - middleY;
10599       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
10600       this.magnetismEnabled = preferences.isMagnetismEnabled()
10601           ^ wasMagnetismToggledLastMousePress();
10602       PlanView planView = getView();
10603       planView.setResizeIndicatorVisible(true);
10604       planView.setToolTipFeedback(getToolTipFeedbackText(this.selectedWall, false),
10605           getXLastMousePress(), getYLastMousePress());
10606       showWallAngleFeedback(this.selectedWall, false);
10607     }
10608 
10609     @Override
10610     public void moveMouse(float x, float y) {
10611       PlanView planView = getView();
10612       float newX = x - this.deltaXToMiddlePoint;
10613       float newY = y - this.deltaYToMiddlePoint;
10614       float arcExtent = getArcExtent(this.selectedWall.getXStart(), this.selectedWall.getYStart(),
10615           this.selectedWall.getXEnd(), this.selectedWall.getYEnd(), newX, newY);
10616       if (this.alignmentActivated
10617           || this.magnetismEnabled) {
10618         arcExtent = (float)Math.toRadians(Math.round(Math.toDegrees(arcExtent)));
10619       }
10620       this.selectedWall.setArcExtent(arcExtent);
10621 
10622       planView.setToolTipFeedback(getToolTipFeedbackText(this.selectedWall, false), x, y);
10623       showWallAngleFeedback(this.selectedWall, false);
10624       // Ensure point at (x,y) is visible
10625       planView.makePointVisible(x, y);
10626     }
10627 
10628     @Override
10629     public void releaseMouse(float x, float y) {
10630       postWallArcExtent(this.selectedWall, this.oldArcExtent);
10631       setState(getSelectionState());
10632     }
10633 
10634     @Override
10635     public void toggleMagnetism(boolean magnetismToggled) {
10636       this.magnetismEnabled = preferences.isMagnetismEnabled()
10637                               ^ magnetismToggled;
10638       moveMouse(getXLastMouseMove(), getYLastMouseMove());
10639     }
10640 
10641     @Override
10642     public void setAlignmentActivated(boolean alignmentActivated) {
10643       this.alignmentActivated = alignmentActivated;
10644       moveMouse(getXLastMouseMove(), getYLastMouseMove());
10645     }
10646 
10647     @Override
10648     public void escape() {
10649       this.selectedWall.setArcExtent(this.oldArcExtent);
10650       setState(getSelectionState());
10651     }
10652 
10653     @Override
10654     public void exit() {
10655       PlanView planView = getView();
10656       planView.setResizeIndicatorVisible(false);
10657       planView.deleteFeedback();
10658       this.selectedWall = null;
10659     }
10660   }
10661 
10662   /**
10663    * Furniture rotation state. This states manages the rotation of a piece of furniture around the vertical axis.
10664    */
10665   private class PieceOfFurnitureRotationState extends ControllerState {
10666     private static final int     STEP_COUNT = 24;
10667     private boolean              magnetismEnabled;
10668     private boolean              alignmentActivated;
10669     private HomePieceOfFurniture selectedPiece;
10670     private float                angleMousePress;
10671     private float                oldAngle;
10672     private boolean              doorOrWindowBoundToWall;
10673     private String               rotationToolTipFeedback;
10674 
10675     @Override
10676     public Mode getMode() {
10677       return Mode.SELECTION;
10678     }
10679 
10680     @Override
10681     public boolean isModificationState() {
10682       return true;
10683     }
10684 
10685     @Override
10686     public boolean isBasePlanModificationState() {
10687       return this.selectedPiece != null
10688           && isPieceOfFurniturePartOfBasePlan(this.selectedPiece);
10689     }
10690 
10691     @Override
10692     public void enter() {
10693       this.rotationToolTipFeedback = preferences.getLocalizedString(
10694           PlanController.class, "rotationToolTipFeedback");
10695       this.selectedPiece = (HomePieceOfFurniture)home.getSelectedItems().get(0);
10696       this.angleMousePress = (float)Math.atan2(this.selectedPiece.getY() - getYLastMousePress(),
10697           getXLastMousePress() - this.selectedPiece.getX());
10698       this.oldAngle = this.selectedPiece.getAngle();
10699       this.doorOrWindowBoundToWall = this.selectedPiece instanceof HomeDoorOrWindow
10700           && ((HomeDoorOrWindow)this.selectedPiece).isBoundToWall();
10701       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
10702       this.magnetismEnabled = preferences.isMagnetismEnabled()
10703                               ^ wasMagnetismToggledLastMousePress();
10704       PlanView planView = getView();
10705       planView.setResizeIndicatorVisible(true);
10706       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldAngle),
10707           getXLastMousePress(), getYLastMousePress());
10708     }
10709 
10710     @Override
10711     public void moveMouse(float x, float y) {
10712       if (x != this.selectedPiece.getX() || y != this.selectedPiece.getY()) {
10713         // Compute the new angle of the piece
10714         float angleMouseMove = (float)Math.atan2(this.selectedPiece.getY() - y,
10715             x - this.selectedPiece.getX());
10716         float newAngle = this.oldAngle - angleMouseMove + this.angleMousePress;
10717 
10718         if (this.alignmentActivated
10719             || this.magnetismEnabled) {
10720           float angleStep = 2 * (float)Math.PI / STEP_COUNT;
10721           // Compute angles closest to a step angle (multiple of angleStep)
10722           newAngle = Math.round(newAngle / angleStep) * angleStep;
10723         }
10724 
10725         // Update piece new angle
10726         this.selectedPiece.setAngle(newAngle);
10727 
10728         // Ensure point at (x,y) is visible
10729         PlanView planView = getView();
10730         planView.makePointVisible(x, y);
10731         planView.setToolTipFeedback(getToolTipFeedbackText(newAngle), x, y);
10732       }
10733     }
10734 
10735     @Override
10736     public void releaseMouse(float x, float y) {
10737       postPieceOfFurnitureRotation(this.selectedPiece, this.oldAngle, this.doorOrWindowBoundToWall);
10738       setState(getSelectionState());
10739     }
10740 
10741     @Override
10742     public void toggleMagnetism(boolean magnetismToggled) {
10743       // Compute active magnetism
10744       this.magnetismEnabled = preferences.isMagnetismEnabled()
10745                               ^ magnetismToggled;
10746       // Compute again angle as if mouse moved
10747       moveMouse(getXLastMouseMove(), getYLastMouseMove());
10748     }
10749 
10750     @Override
10751     public void setAlignmentActivated(boolean alignmentActivated) {
10752       this.alignmentActivated = alignmentActivated;
10753       moveMouse(getXLastMouseMove(), getYLastMouseMove());
10754     }
10755 
10756     @Override
10757     public void escape() {
10758       this.selectedPiece.setAngle(this.oldAngle);
10759       if (this.selectedPiece instanceof HomeDoorOrWindow) {
10760         ((HomeDoorOrWindow)this.selectedPiece).setBoundToWall(this.doorOrWindowBoundToWall);
10761       }
10762       setState(getSelectionState());
10763     }
10764 
10765     @Override
10766     public void exit() {
10767       PlanView planView = getView();
10768       planView.setResizeIndicatorVisible(false);
10769       planView.deleteFeedback();
10770       this.selectedPiece = null;
10771     }
10772 
10773     private String getToolTipFeedbackText(float angle) {
10774       return String.format(this.rotationToolTipFeedback, (Math.round(Math.toDegrees(angle)) + 360) % 360);
10775     }
10776   }
10777 
10778   /**
10779    * Furniture pitch rotation state. This states manages the rotation of a piece of furniture
10780    * around the horizontal pitch (transversal) axis.
10781    */
10782   private class PieceOfFurniturePitchRotationState extends ControllerState {
10783     private HomePieceOfFurniture selectedPiece;
10784     private float                oldPitch;
10785     private float                oldWidthInPlan;
10786     private float                oldDepthInPlan;
10787     private float                oldHeightInPlan;
10788     private String               pitchRotationToolTipFeedback;
10789 
10790     @Override
10791     public Mode getMode() {
10792       return Mode.SELECTION;
10793     }
10794 
10795     @Override
10796     public boolean isModificationState() {
10797       return true;
10798     }
10799 
10800     @Override
10801     public boolean isBasePlanModificationState() {
10802       return this.selectedPiece != null
10803           && isPieceOfFurniturePartOfBasePlan(this.selectedPiece);
10804     }
10805 
10806     @Override
10807     public void enter() {
10808       this.pitchRotationToolTipFeedback = preferences.getLocalizedString(
10809           PlanController.class, "pitchRotationToolTipFeedback");
10810       this.selectedPiece = (HomePieceOfFurniture)home.getSelectedItems().get(0);
10811       this.oldPitch = this.selectedPiece.getPitch();
10812       this.oldWidthInPlan = this.selectedPiece.getWidthInPlan();
10813       this.oldDepthInPlan = this.selectedPiece.getDepthInPlan();
10814       this.oldHeightInPlan = this.selectedPiece.getHeightInPlan();
10815       PlanView planView = getView();
10816       planView.setResizeIndicatorVisible(true);
10817       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldPitch),
10818           getXLastMousePress(), getYLastMousePress());
10819     }
10820 
10821     @Override
10822     public void moveMouse(float x, float y) {
10823       // Compute the new pitch angle of the piece
10824       float newPitch = (float)(this.oldPitch
10825           - (y - getYLastMousePress()) * Math.cos(this.selectedPiece.getAngle()) * Math.PI / 360
10826           + (x - getXLastMousePress()) * Math.sin(this.selectedPiece.getAngle()) * Math.PI / 360);
10827       if (Math.abs(newPitch) < 1E-8) {
10828         newPitch = 0;
10829       }
10830       // Update pitch angle
10831       this.selectedPiece.setPitch(newPitch);
10832       getView().setToolTipFeedback(getToolTipFeedbackText(newPitch), x, y);
10833     }
10834 
10835     @Override
10836     public void releaseMouse(float x, float y) {
10837       postPieceOfFurniturePitchRotation(this.selectedPiece, this.oldPitch, this.oldWidthInPlan, this.oldDepthInPlan, this.oldHeightInPlan);
10838       setState(getSelectionState());
10839     }
10840 
10841     @Override
10842     public void escape() {
10843       this.selectedPiece.setPitch(this.oldPitch);
10844       setState(getSelectionState());
10845     }
10846 
10847     @Override
10848     public void exit() {
10849       PlanView planView = getView();
10850       planView.setResizeIndicatorVisible(false);
10851       planView.deleteFeedback();
10852       this.selectedPiece = null;
10853     }
10854 
10855     private String getToolTipFeedbackText(float pitch) {
10856       return String.format(this.pitchRotationToolTipFeedback, (Math.round(Math.toDegrees(pitch)) + 360) % 360);
10857     }
10858   }
10859 
10860   /**
10861    * Furniture roll rotation state. This states manages the rotation of a piece of furniture
10862    * around the horizontal roll axis.
10863    */
10864   private class PieceOfFurnitureRollRotationState extends ControllerState {
10865     private HomePieceOfFurniture selectedPiece;
10866     private float                oldRoll;
10867     private float                oldWidthInPlan;
10868     private float                oldDepthInPlan;
10869     private float                oldHeightInPlan;
10870     private String               rollRotationToolTipFeedback;
10871 
10872     @Override
10873     public Mode getMode() {
10874       return Mode.SELECTION;
10875     }
10876 
10877     @Override
10878     public boolean isModificationState() {
10879       return true;
10880     }
10881 
10882     @Override
10883     public boolean isBasePlanModificationState() {
10884       return this.selectedPiece != null
10885           && isPieceOfFurniturePartOfBasePlan(this.selectedPiece);
10886     }
10887 
10888     @Override
10889     public void enter() {
10890       this.rollRotationToolTipFeedback = preferences.getLocalizedString(
10891           PlanController.class, "rollRotationToolTipFeedback");
10892       this.selectedPiece = (HomePieceOfFurniture)home.getSelectedItems().get(0);
10893       this.oldRoll = this.selectedPiece.getRoll();
10894       this.oldWidthInPlan = this.selectedPiece.getWidthInPlan();
10895       this.oldDepthInPlan = this.selectedPiece.getDepthInPlan();
10896       this.oldHeightInPlan = this.selectedPiece.getHeightInPlan();
10897       PlanView planView = getView();
10898       planView.setResizeIndicatorVisible(true);
10899       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldRoll),
10900           getXLastMousePress(), getYLastMousePress());
10901     }
10902 
10903     @Override
10904     public void moveMouse(float x, float y) {
10905       // Compute the new roll angle of the piece
10906       float newRoll = (float)(this.oldRoll
10907           + (y - getYLastMousePress()) * Math.sin(this.selectedPiece.getAngle()) * Math.PI / 360
10908           + (x - getXLastMousePress()) * Math.cos(this.selectedPiece.getAngle()) * Math.PI / 360);
10909       if (Math.abs(newRoll) < 1E-8) {
10910         newRoll = 0;
10911       }
10912       // Update roll angle
10913       this.selectedPiece.setRoll(newRoll);
10914       getView().setToolTipFeedback(getToolTipFeedbackText(newRoll), x, y);
10915     }
10916 
10917     @Override
10918     public void releaseMouse(float x, float y) {
10919       postPieceOfFurnitureRollRotation(this.selectedPiece, this.oldRoll, this.oldWidthInPlan, this.oldDepthInPlan, this.oldHeightInPlan);
10920       setState(getSelectionState());
10921     }
10922 
10923     @Override
10924     public void escape() {
10925       this.selectedPiece.setRoll(this.oldRoll);
10926       setState(getSelectionState());
10927     }
10928 
10929     @Override
10930     public void exit() {
10931       PlanView planView = getView();
10932       planView.setResizeIndicatorVisible(false);
10933       planView.deleteFeedback();
10934       this.selectedPiece = null;
10935     }
10936 
10937     private String getToolTipFeedbackText(float roll) {
10938       return String.format(this.rollRotationToolTipFeedback, (Math.round(Math.toDegrees(roll)) + 360) % 360);
10939     }
10940   }
10941 
10942   /**
10943    * Furniture elevation state. This states manages the elevation change of a piece of furniture.
10944    */
10945   private class PieceOfFurnitureElevationState extends ControllerState {
10946     private boolean              magnetismEnabled;
10947     private float                deltaYToElevationPoint;
10948     private HomePieceOfFurniture selectedPiece;
10949     private float                oldElevation;
10950     private String               elevationToolTipFeedback;
10951 
10952     @Override
10953     public Mode getMode() {
10954       return Mode.SELECTION;
10955     }
10956 
10957     @Override
10958     public boolean isModificationState() {
10959       return true;
10960     }
10961 
10962     @Override
10963     public boolean isBasePlanModificationState() {
10964       return this.selectedPiece != null
10965           && isPieceOfFurniturePartOfBasePlan(this.selectedPiece);
10966     }
10967 
10968     @Override
10969     public void enter() {
10970       this.elevationToolTipFeedback = preferences.getLocalizedString(
10971           PlanController.class, "elevationToolTipFeedback");
10972       this.selectedPiece = (HomePieceOfFurniture)home.getSelectedItems().get(0);
10973       float [] elevationPoint = this.selectedPiece.getPoints() [1];
10974       this.deltaYToElevationPoint = getYLastMousePress() - elevationPoint [1];
10975       this.oldElevation = this.selectedPiece.getElevation();
10976       this.magnetismEnabled = preferences.isMagnetismEnabled()
10977                               ^ wasMagnetismToggledLastMousePress();
10978       PlanView planView = getView();
10979       planView.setResizeIndicatorVisible(true);
10980       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldElevation),
10981           getXLastMousePress(), getYLastMousePress());
10982     }
10983 
10984     @Override
10985     public void moveMouse(float x, float y) {
10986       // Compute the new elevation of the piece
10987       PlanView planView = getView();
10988       float [] topRightPoint = this.selectedPiece.getPoints() [1];
10989       float deltaY = y - this.deltaYToElevationPoint - topRightPoint[1];
10990       float newElevation = this.oldElevation - deltaY;
10991       newElevation = Math.min(Math.max(newElevation, 0f), preferences.getLengthUnit().getMaximumElevation());
10992       if (this.magnetismEnabled) {
10993         newElevation = preferences.getLengthUnit().getMagnetizedLength(newElevation, planView.getPixelLength());
10994       }
10995 
10996       // Update piece new dimension
10997       this.selectedPiece.setElevation(newElevation);
10998 
10999       // Ensure point at (x,y) is visible
11000       planView.makePointVisible(x, y);
11001       planView.setToolTipFeedback(getToolTipFeedbackText(newElevation), x, y);
11002     }
11003 
11004     @Override
11005     public void releaseMouse(float x, float y) {
11006       postPieceOfFurnitureElevation(this.selectedPiece, this.oldElevation);
11007       setState(getSelectionState());
11008     }
11009 
11010     @Override
11011     public void toggleMagnetism(boolean magnetismToggled) {
11012       // Compute active magnetism
11013       this.magnetismEnabled = preferences.isMagnetismEnabled()
11014                               ^ magnetismToggled;
11015       // Compute again angle as if mouse moved
11016       moveMouse(getXLastMouseMove(), getYLastMouseMove());
11017     }
11018 
11019     @Override
11020     public void escape() {
11021       this.selectedPiece.setElevation(this.oldElevation);
11022       setState(getSelectionState());
11023     }
11024 
11025     @Override
11026     public void exit() {
11027       PlanView planView = getView();
11028       planView.setResizeIndicatorVisible(false);
11029       planView.deleteFeedback();
11030       this.selectedPiece = null;
11031     }
11032 
11033     private String getToolTipFeedbackText(float height) {
11034       return String.format(this.elevationToolTipFeedback,
11035           preferences.getLengthUnit().getFormatWithUnit().format(height));
11036     }
11037   }
11038 
11039   /**
11040    * Furniture height state. This states manages the height resizing of a piece of furniture.
11041    * Caution: Do not use for furniture with a roll or pitch angle different from 0
11042    */
11043   private class PieceOfFurnitureHeightState extends ControllerState {
11044     private boolean                 magnetismEnabled;
11045     private float                   deltaYToResizePoint;
11046     private ResizedPieceOfFurniture resizedPiece;
11047     private float []                topLeftPoint;
11048     private float []                resizePoint;
11049     private String                  resizeToolTipFeedback;
11050 
11051     @Override
11052     public Mode getMode() {
11053       return Mode.SELECTION;
11054     }
11055 
11056     @Override
11057     public boolean isModificationState() {
11058       return true;
11059     }
11060 
11061     @Override
11062     public boolean isBasePlanModificationState() {
11063       return this.resizedPiece != null
11064           && isPieceOfFurniturePartOfBasePlan(this.resizedPiece.getPieceOfFurniture());
11065     }
11066 
11067     @Override
11068     public void enter() {
11069       this.resizeToolTipFeedback = preferences.getLocalizedString(
11070           PlanController.class, "heightResizeToolTipFeedback");
11071       HomePieceOfFurniture selectedPiece = (HomePieceOfFurniture)home.getSelectedItems().get(0);
11072       this.resizedPiece = new ResizedPieceOfFurniture(selectedPiece);
11073       float [][] resizedPiecePoints = selectedPiece.getPoints();
11074       this.resizePoint = resizedPiecePoints [3];
11075       this.deltaYToResizePoint = getYLastMousePress() - this.resizePoint [1];
11076       this.topLeftPoint = resizedPiecePoints [0];
11077       this.magnetismEnabled = preferences.isMagnetismEnabled()
11078                               ^ wasMagnetismToggledLastMousePress();
11079       PlanView planView = getView();
11080       planView.setResizeIndicatorVisible(true);
11081       planView.setToolTipFeedback(getToolTipFeedbackText(selectedPiece.getHeight()),
11082           getXLastMousePress(), getYLastMousePress());
11083     }
11084 
11085     @Override
11086     public void moveMouse(float x, float y) {
11087       // Compute the new height of the piece
11088       PlanView planView = getView();
11089       HomePieceOfFurniture selectedPiece = this.resizedPiece.getPieceOfFurniture();
11090       float deltaY = y - this.deltaYToResizePoint - this.resizePoint [1];
11091       float newHeight = this.resizedPiece.getHeight() - deltaY;
11092       newHeight = Math.max(newHeight, 0f);
11093       if (this.magnetismEnabled) {
11094         newHeight = preferences.getLengthUnit().getMagnetizedLength(newHeight, planView.getPixelLength());
11095       }
11096       newHeight = Math.min(Math.max(newHeight, preferences.getLengthUnit().getMinimumLength()),
11097           preferences.getLengthUnit().getMaximumLength());
11098 
11099       if (selectedPiece.isDeformable()
11100           && !selectedPiece.isHorizontallyRotated()
11101           && selectedPiece.getModelTransformations() == null) {
11102         // Update piece new dimension
11103         setPieceOfFurnitureSize(this.resizedPiece,
11104             this.resizedPiece.getWidth(), this.resizedPiece.getDepth(), newHeight);
11105       } else {
11106         // Manage proportional constraint
11107         float scale = newHeight / this.resizedPiece.getHeight();
11108         float newWidth = this.resizedPiece.getWidth() * scale;
11109         float newDepth = this.resizedPiece.getDepth() * scale;
11110         // Update piece new location
11111         float angle = selectedPiece.getAngle();
11112         double cos = Math.cos(angle);
11113         double sin = Math.sin(angle);
11114         float newX = (float)(this.topLeftPoint [0] + (newWidth * cos - newDepth * sin) / 2f);
11115         float newY = (float)(this.topLeftPoint [1] + (newWidth * sin + newDepth * cos) / 2f);
11116         selectedPiece.setX(newX);
11117         selectedPiece.setY(newY);
11118         setPieceOfFurnitureSize(this.resizedPiece, newWidth, newDepth, newHeight);
11119       }
11120 
11121       // Ensure point at (x,y) is visible
11122       planView.makePointVisible(x, y);
11123       planView.setToolTipFeedback(getToolTipFeedbackText(newHeight), x, y);
11124     }
11125 
11126     @Override
11127     public void releaseMouse(float x, float y) {
11128       postPieceOfFurnitureHeightResize(this.resizedPiece);
11129       setState(getSelectionState());
11130     }
11131 
11132     @Override
11133     public void toggleMagnetism(boolean magnetismToggled) {
11134       // Compute active magnetism
11135       this.magnetismEnabled = preferences.isMagnetismEnabled()
11136                               ^ magnetismToggled;
11137       // Compute again angle as if mouse moved
11138       moveMouse(getXLastMouseMove(), getYLastMouseMove());
11139     }
11140 
11141     @Override
11142     public void escape() {
11143       resetPieceOfFurnitureSize(this.resizedPiece);
11144       setState(getSelectionState());
11145     }
11146 
11147     @Override
11148     public void exit() {
11149       PlanView planView = getView();
11150       planView.setResizeIndicatorVisible(false);
11151       planView.deleteFeedback();
11152       this.resizedPiece = null;
11153     }
11154 
11155     private String getToolTipFeedbackText(float height) {
11156       return String.format(this.resizeToolTipFeedback,
11157           preferences.getLengthUnit().getFormatWithUnit().format(height));
11158     }
11159   }
11160 
11161   /**
11162    * Furniture resize state. This states manages the resizing of a piece of furniture.
11163    */
11164   private class PieceOfFurnitureResizeState extends ControllerState {
11165     private boolean                 magnetismEnabled;
11166     private boolean                 alignmentActivated;
11167     private boolean                 widthOrDepthResizingActivated;
11168     private float                   deltaXToResizePoint;
11169     private float                   deltaYToResizePoint;
11170     private ResizedPieceOfFurniture resizedPiece;
11171     private float                   resizedPieceWidthInPlan;
11172     private float []                resizePoint;
11173     private float []                topLeftPoint;
11174     private String                  widthResizeToolTipFeedback;
11175     private String                  depthResizeToolTipFeedback;
11176     private String                  heightResizeToolTipFeedback;
11177 
11178     @Override
11179     public Mode getMode() {
11180       return Mode.SELECTION;
11181     }
11182 
11183     @Override
11184     public boolean isModificationState() {
11185       return true;
11186     }
11187 
11188     @Override
11189     public boolean isBasePlanModificationState() {
11190       return this.resizedPiece != null
11191           && isPieceOfFurniturePartOfBasePlan(this.resizedPiece.getPieceOfFurniture());
11192     }
11193 
11194     @Override
11195     public void enter() {
11196       this.widthResizeToolTipFeedback = preferences.getLocalizedString(
11197           PlanController.class, "widthResizeToolTipFeedback");
11198       this.depthResizeToolTipFeedback = preferences.getLocalizedString(
11199           PlanController.class, "depthResizeToolTipFeedback");
11200       this.heightResizeToolTipFeedback = preferences.getLocalizedString(
11201           PlanController.class, "heightResizeToolTipFeedback");
11202       HomePieceOfFurniture selectedPiece = (HomePieceOfFurniture)home.getSelectedItems().get(0);
11203       this.resizedPiece = new ResizedPieceOfFurniture(selectedPiece);
11204       this.resizedPieceWidthInPlan = selectedPiece.getWidthInPlan();
11205       float [][] resizedPiecePoints = selectedPiece.getPoints();
11206       this.resizePoint = resizedPiecePoints [2];
11207       this.deltaXToResizePoint = getXLastMousePress() - this.resizePoint [0];
11208       this.deltaYToResizePoint = getYLastMousePress() - this.resizePoint [1];
11209       this.topLeftPoint = resizedPiecePoints [0];
11210       this.magnetismEnabled = preferences.isMagnetismEnabled()
11211                               ^ wasMagnetismToggledLastMousePress();
11212       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
11213       this.widthOrDepthResizingActivated = wasDuplicationActivatedLastMousePress();
11214       PlanView planView = getView();
11215       planView.setResizeIndicatorVisible(true);
11216       planView.setToolTipFeedback(getToolTipFeedbackText(selectedPiece.getWidth(), selectedPiece.getDepth(), selectedPiece.getHeight()),
11217           getXLastMousePress(), getYLastMousePress());
11218     }
11219 
11220     @Override
11221     public void moveMouse(float x, float y) {
11222       // Compute the new location and dimension of the piece to let
11223       // its bottom right point be at mouse location
11224       PlanView planView = getView();
11225       HomePieceOfFurniture selectedPiece = this.resizedPiece.getPieceOfFurniture();
11226       float angle = selectedPiece.getAngle();
11227       double cos = Math.cos(angle);
11228       double sin = Math.sin(angle);
11229       float deltaX = x - this.deltaXToResizePoint - this.topLeftPoint [0];
11230       float deltaY = y - this.deltaYToResizePoint - this.topLeftPoint [1];
11231       float newWidth =  (float)(deltaY * sin + deltaX * cos);
11232       if (this.magnetismEnabled) {
11233         newWidth = preferences.getLengthUnit().getMagnetizedLength(newWidth, planView.getPixelLength());
11234       }
11235       newWidth = Math.min(Math.max(newWidth, preferences.getLengthUnit().getMinimumLength()),
11236           preferences.getLengthUnit().getMaximumLength());
11237 
11238       float newDepth = this.resizedPiece.getDepth();
11239       float newHeight = this.resizedPiece.getHeight();
11240       boolean doorOrWindowBoundToWall = this.resizedPiece.isDoorOrWindowBoundToWall();
11241       // Manage constraints
11242       if (isProprortionallyResized(selectedPiece)) {
11243         // Use resizing scale based on width in plan
11244         float scale = newWidth / this.resizedPieceWidthInPlan;
11245         newWidth = this.resizedPiece.getWidth() * scale;
11246         newDepth = this.resizedPiece.getDepth() * scale;
11247         newHeight = this.resizedPiece.getHeight() * scale;
11248         doorOrWindowBoundToWall = newDepth == this.resizedPiece.getDepth();
11249       } else if (!selectedPiece.isWidthDepthDeformable()) {
11250         newDepth = this.resizedPiece.getDepth() * newWidth / this.resizedPiece.getWidth();
11251       } else if (!this.resizedPiece.isDoorOrWindowBoundToWall()
11252                  || !this.magnetismEnabled
11253                  || this.widthOrDepthResizingActivated) {
11254         // Update piece depth if it's not a door a window
11255         // or if it's a a door a window unbound to a wall when magnetism is enabled
11256         newDepth = (float)(deltaY * cos - deltaX * sin);
11257         if (this.magnetismEnabled) {
11258           newDepth = preferences.getLengthUnit().getMagnetizedLength(newDepth, planView.getPixelLength());
11259         }
11260         newDepth = Math.min(Math.max(newDepth, preferences.getLengthUnit().getMinimumLength()),
11261             preferences.getLengthUnit().getMaximumLength());
11262         doorOrWindowBoundToWall = newDepth == this.resizedPiece.getDepth();
11263 
11264         if (this.widthOrDepthResizingActivated) {
11265           // Allow resizing of only width or depth depending on the location of the cursor
11266           // above or below a line joining the two opposite corners of the resized piece
11267           if (Math.signum(Line2D.relativeCCW(this.topLeftPoint [0], this.topLeftPoint [1],
11268               this.resizePoint [0], this.resizePoint [1],
11269               x - this.deltaXToResizePoint, y - this.deltaYToResizePoint)) >= 0) {
11270             newDepth = this.resizedPiece.getDepth();
11271           } else {
11272             newWidth = this.resizedPiece.getWidth();
11273           }
11274         }
11275       }
11276 
11277       // Update piece size
11278       setPieceOfFurnitureSize(this.resizedPiece, newWidth, newDepth, newHeight);
11279       if (this.resizedPiece.isDoorOrWindowBoundToWall()) {
11280         // Maintain boundToWall flag
11281         ((HomeDoorOrWindow)selectedPiece).setBoundToWall(this.magnetismEnabled && doorOrWindowBoundToWall);
11282       }
11283 
11284       // Update piece new location
11285       float newX = (float)(this.topLeftPoint [0] + (selectedPiece.getWidthInPlan() * cos - selectedPiece.getDepthInPlan() * sin) / 2f);
11286       float newY = (float)(this.topLeftPoint [1] + (selectedPiece.getWidthInPlan() * sin + selectedPiece.getDepthInPlan() * cos) / 2f);
11287       selectedPiece.setX(newX);
11288       selectedPiece.setY(newY);
11289 
11290       // Ensure point at (x,y) is visible
11291       planView.makePointVisible(x, y);
11292       planView.setToolTipFeedback(getToolTipFeedbackText(newWidth, newDepth, newHeight), x, y);
11293     }
11294 
11295     /**
11296      * Returns <code>true</code> if the <code>piece</code> should be proportionally resized.
11297      */
11298     private boolean isProprortionallyResized(HomePieceOfFurniture piece) {
11299       return !piece.isDeformable()
11300           || piece.isHorizontallyRotated()
11301           || piece.getModelTransformations() != null
11302           || this.alignmentActivated;
11303     }
11304 
11305     @Override
11306     public void releaseMouse(float x, float y) {
11307       postPieceOfFurnitureWidthAndDepthResize(this.resizedPiece);
11308       setState(getSelectionState());
11309     }
11310 
11311     @Override
11312     public void toggleMagnetism(boolean magnetismToggled) {
11313       // Compute active magnetism
11314       this.magnetismEnabled = preferences.isMagnetismEnabled()
11315                               ^ magnetismToggled;
11316       // Compute again angle as if mouse moved
11317       moveMouse(getXLastMouseMove(), getYLastMouseMove());
11318     }
11319 
11320     @Override
11321     public void setAlignmentActivated(boolean alignmentActivated) {
11322       this.alignmentActivated = alignmentActivated;
11323       moveMouse(getXLastMouseMove(), getYLastMouseMove());
11324     }
11325 
11326     @Override
11327     public void setDuplicationActivated(boolean duplicationActivated) {
11328       this.widthOrDepthResizingActivated = duplicationActivated;
11329       moveMouse(getXLastMouseMove(), getYLastMouseMove());
11330     }
11331 
11332     @Override
11333     public void escape() {
11334       resetPieceOfFurnitureSize(this.resizedPiece);
11335       setState(getSelectionState());
11336     }
11337 
11338     @Override
11339     public void exit() {
11340       PlanView planView = getView();
11341       planView.setResizeIndicatorVisible(false);
11342       planView.deleteFeedback();
11343       this.resizedPiece = null;
11344     }
11345 
11346     private String getToolTipFeedbackText(float width, float depth, float height) {
11347       String toolTipFeedbackText = "<html>" + String.format(this.widthResizeToolTipFeedback,
11348           preferences.getLengthUnit().getFormatWithUnit().format(width));
11349       if (!(this.resizedPiece.getPieceOfFurniture() instanceof HomeDoorOrWindow)
11350           || !((HomeDoorOrWindow)this.resizedPiece.getPieceOfFurniture()).isBoundToWall()
11351           || isProprortionallyResized(this.resizedPiece.getPieceOfFurniture())) {
11352         toolTipFeedbackText += "<br>" + String.format(this.depthResizeToolTipFeedback,
11353             preferences.getLengthUnit().getFormatWithUnit().format(depth));
11354       }
11355       if (isProprortionallyResized(this.resizedPiece.getPieceOfFurniture())) {
11356         toolTipFeedbackText += "<br>" + String.format(this.heightResizeToolTipFeedback,
11357             preferences.getLengthUnit().getFormatWithUnit().format(height));
11358       }
11359       return toolTipFeedbackText;
11360     }
11361   }
11362 
11363   /**
11364    * Light power state. This states manages the power modification of a light.
11365    */
11366   private class LightPowerModificationState extends ControllerState {
11367     private float     deltaXToModificationPoint;
11368     private HomeLight selectedLight;
11369     private float     oldPower;
11370     private String    lightPowerToolTipFeedback;
11371 
11372     @Override
11373     public Mode getMode() {
11374       return Mode.SELECTION;
11375     }
11376 
11377     @Override
11378     public boolean isModificationState() {
11379       return true;
11380     }
11381 
11382     @Override
11383     public void enter() {
11384       this.lightPowerToolTipFeedback = preferences.getLocalizedString(
11385           PlanController.class, "lightPowerToolTipFeedback");
11386       this.selectedLight = (HomeLight)home.getSelectedItems().get(0);
11387       float [] resizePoint = this.selectedLight.getPoints() [3];
11388       this.deltaXToModificationPoint = getXLastMousePress() - resizePoint [0];
11389       this.oldPower = this.selectedLight.getPower();
11390       PlanView planView = getView();
11391       planView.setResizeIndicatorVisible(true);
11392       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldPower),
11393           getXLastMousePress(), getYLastMousePress());
11394     }
11395 
11396     @Override
11397     public void moveMouse(float x, float y) {
11398       // Compute the new power of the light
11399       PlanView planView = getView();
11400       float [] bottomLeftPoint = this.selectedLight.getPoints() [3];
11401       float deltaX = x - this.deltaXToModificationPoint - bottomLeftPoint [0];
11402       float newPower = this.oldPower + deltaX / 100f * getScale();
11403       newPower = Math.min(Math.max(newPower, 0f), 1f);
11404       // Update light power
11405       this.selectedLight.setPower(newPower);
11406 
11407       // Ensure point at (x,y) is visible
11408       planView.makePointVisible(x, y);
11409       planView.setToolTipFeedback(getToolTipFeedbackText(newPower), x, y);
11410     }
11411 
11412     @Override
11413     public void releaseMouse(float x, float y) {
11414       postLightPowerModification(this.selectedLight, this.oldPower);
11415       setState(getSelectionState());
11416     }
11417 
11418     @Override
11419     public void escape() {
11420       this.selectedLight.setPower(this.oldPower);
11421       setState(getSelectionState());
11422     }
11423 
11424     @Override
11425     public void exit() {
11426       PlanView planView = getView();
11427       planView.setResizeIndicatorVisible(false);
11428       planView.deleteFeedback();
11429       this.selectedLight = null;
11430     }
11431 
11432     private String getToolTipFeedbackText(float power) {
11433       return String.format(this.lightPowerToolTipFeedback, Math.round(power * 100));
11434     }
11435   }
11436 
11437   /**
11438    * Furniture name offset state. This state manages the name offset of a piece of furniture.
11439    */
11440   private class PieceOfFurnitureNameOffsetState extends ControllerState {
11441     private HomePieceOfFurniture selectedPiece;
11442     private float                oldNameXOffset;
11443     private float                oldNameYOffset;
11444     private float                xLastMouseMove;
11445     private float                yLastMouseMove;
11446     private boolean              alignmentActivated;
11447 
11448     @Override
11449     public Mode getMode() {
11450       return Mode.SELECTION;
11451     }
11452 
11453     @Override
11454     public boolean isModificationState() {
11455       return true;
11456     }
11457 
11458     @Override
11459     public void enter() {
11460       this.selectedPiece = (HomePieceOfFurniture)home.getSelectedItems().get(0);
11461       this.oldNameXOffset = this.selectedPiece.getNameXOffset();
11462       this.oldNameYOffset = this.selectedPiece.getNameYOffset();
11463       this.xLastMouseMove = getXLastMousePress();
11464       this.yLastMouseMove = getYLastMousePress();
11465       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
11466       PlanView planView = getView();
11467       planView.setResizeIndicatorVisible(true);
11468     }
11469 
11470     @Override
11471     public void moveMouse(float x, float y) {
11472       if (this.alignmentActivated) {
11473         PointWithAngleMagnetism alignedPoint = new PointWithAngleMagnetism(getXLastMousePress(), getYLastMousePress(),
11474             x, y, preferences.getLengthUnit(), getView().getPixelLength(), 4);
11475         x = alignedPoint.getX();
11476         y = alignedPoint.getY();
11477       }
11478       this.selectedPiece.setNameXOffset(this.selectedPiece.getNameXOffset() + x - this.xLastMouseMove);
11479       this.selectedPiece.setNameYOffset(this.selectedPiece.getNameYOffset() + y - this.yLastMouseMove);
11480       this.xLastMouseMove = x;
11481       this.yLastMouseMove = y;
11482 
11483       // Ensure point at (x,y) is visible
11484       getView().makePointVisible(x, y);
11485     }
11486 
11487     @Override
11488     public void releaseMouse(float x, float y) {
11489       postPieceOfFurnitureNameOffset(this.selectedPiece, this.oldNameXOffset, this.oldNameYOffset);
11490       setState(getSelectionState());
11491     }
11492 
11493     @Override
11494     public void escape() {
11495       this.selectedPiece.setNameXOffset(this.oldNameXOffset);
11496       this.selectedPiece.setNameYOffset(this.oldNameYOffset);
11497       setState(getSelectionState());
11498     }
11499 
11500     @Override
11501     public void setAlignmentActivated(boolean alignmentActivated) {
11502       this.alignmentActivated = alignmentActivated;
11503       moveMouse(getXLastMouseMove(), getYLastMouseMove());
11504     }
11505 
11506     @Override
11507     public void exit() {
11508       getView().setResizeIndicatorVisible(false);
11509       this.selectedPiece = null;
11510     }
11511   }
11512 
11513   /**
11514    * Furniture name rotation state. This state manages the name rotation of a piece of furniture.
11515    */
11516   private class PieceOfFurnitureNameRotationState extends ControllerState {
11517     private static final int     STEP_COUNT = 24;
11518 
11519     private HomePieceOfFurniture selectedPiece;
11520     private float                oldNameAngle;
11521     private float                angleMousePress;
11522     private boolean              magnetismEnabled;
11523     private boolean              alignmentActivated;
11524 
11525     @Override
11526     public Mode getMode() {
11527       return Mode.SELECTION;
11528     }
11529 
11530     @Override
11531     public boolean isModificationState() {
11532       return true;
11533     }
11534 
11535     @Override
11536     public void enter() {
11537       this.selectedPiece = (HomePieceOfFurniture)home.getSelectedItems().get(0);
11538       this.angleMousePress = (float)Math.atan2(this.selectedPiece.getY() + this.selectedPiece.getNameYOffset() - getYLastMousePress(),
11539           getXLastMousePress() - this.selectedPiece.getX() - this.selectedPiece.getNameXOffset());
11540       this.oldNameAngle = this.selectedPiece.getNameAngle();
11541       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
11542       this.magnetismEnabled = preferences.isMagnetismEnabled()
11543           ^ wasMagnetismToggledLastMousePress();
11544       PlanView planView = getView();
11545       planView.setResizeIndicatorVisible(true);
11546     }
11547 
11548     @Override
11549     public void moveMouse(float x, float y) {
11550       if (x != this.selectedPiece.getX() + this.selectedPiece.getNameXOffset()
11551           || y != this.selectedPiece.getY() + this.selectedPiece.getNameYOffset()) {
11552         // Compute the new angle of the piece name
11553         float angleMouseMove = (float)Math.atan2(this.selectedPiece.getY() + this.selectedPiece.getNameYOffset() - y,
11554             x - this.selectedPiece.getX() - this.selectedPiece.getNameXOffset());
11555         float newAngle = this.oldNameAngle - angleMouseMove + this.angleMousePress;
11556 
11557         if (this.alignmentActivated
11558             || this.magnetismEnabled) {
11559           float angleStep = 2 * (float)Math.PI / STEP_COUNT;
11560           // Compute angles closest to a step angle (multiple of angleStep)
11561           newAngle = Math.round(newAngle / angleStep) * angleStep;
11562         }
11563 
11564         // Update piece name new angle
11565         this.selectedPiece.setNameAngle(newAngle);
11566         getView().makePointVisible(x, y);
11567       }
11568     }
11569 
11570     @Override
11571     public void releaseMouse(float x, float y) {
11572       postPieceOfFurnitureNameRotation(this.selectedPiece, this.oldNameAngle);
11573       setState(getSelectionState());
11574     }
11575 
11576     @Override
11577     public void toggleMagnetism(boolean magnetismToggled) {
11578       // Compute active magnetism
11579       this.magnetismEnabled = preferences.isMagnetismEnabled()
11580                               ^ magnetismToggled;
11581       // Compute again angle as if mouse moved
11582       moveMouse(getXLastMouseMove(), getYLastMouseMove());
11583     }
11584 
11585     @Override
11586     public void setAlignmentActivated(boolean alignmentActivated) {
11587       this.alignmentActivated = alignmentActivated;
11588       moveMouse(getXLastMouseMove(), getYLastMouseMove());
11589     }
11590 
11591     @Override
11592     public void escape() {
11593       this.selectedPiece.setNameAngle(this.oldNameAngle);
11594       setState(getSelectionState());
11595     }
11596 
11597     @Override
11598     public void exit() {
11599       getView().setResizeIndicatorVisible(false);
11600       this.selectedPiece = null;
11601     }
11602   }
11603 
11604   /**
11605    * Camera yaw change state. This states manages the change of the observer camera yaw angle.
11606    */
11607   private class CameraYawRotationState extends ControllerState {
11608     private ObserverCamera selectedCamera;
11609     private float          oldYaw;
11610     private float          xLastMouseMove;
11611     private float          yLastMouseMove;
11612     private float          angleLastMouseMove;
11613     private String         rotationToolTipFeedback;
11614 
11615     @Override
11616     public Mode getMode() {
11617       return Mode.SELECTION;
11618     }
11619 
11620     @Override
11621     public boolean isModificationState() {
11622       return true;
11623     }
11624 
11625     @Override
11626     public void enter() {
11627       this.rotationToolTipFeedback = preferences.getLocalizedString(
11628           PlanController.class, "cameraYawRotationToolTipFeedback");
11629       this.selectedCamera = (ObserverCamera)home.getSelectedItems().get(0);
11630       this.oldYaw = this.selectedCamera.getYaw();
11631       this.xLastMouseMove = getXLastMousePress();
11632       this.yLastMouseMove = getYLastMousePress();
11633       this.angleLastMouseMove = (float)Math.atan2(this.selectedCamera.getY() - this.yLastMouseMove,
11634           this.xLastMouseMove - this.selectedCamera.getX());
11635       PlanView planView = getView();
11636       planView.setResizeIndicatorVisible(true);
11637       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldYaw),
11638           getXLastMousePress(), getYLastMousePress());
11639     }
11640 
11641     @Override
11642     public void moveMouse(float x, float y) {
11643       if (x != this.selectedCamera.getX() || y != this.selectedCamera.getY()) {
11644         // Compute the new angle of the camera
11645         float angleMouseMove = (float)Math.atan2(this.selectedCamera.getY() - y,
11646             x - this.selectedCamera.getX());
11647 
11648         // Compute yaw angle with a delta that takes into account the direction
11649         // of the rotation (clock wise or counter clock wise)
11650         float deltaYaw = angleLastMouseMove - angleMouseMove;
11651         float orientation = Math.signum((y - this.selectedCamera.getY()) * (this.xLastMouseMove - this.selectedCamera.getX())
11652             - (this.yLastMouseMove - this.selectedCamera.getY()) * (x- this.selectedCamera.getX()));
11653         if (orientation < 0 && deltaYaw > 0) {
11654           deltaYaw -= (float)(Math.PI * 2f);
11655         } else if (orientation > 0 && deltaYaw < 0) {
11656           deltaYaw += (float)(Math.PI * 2f);
11657         }
11658 
11659         // Update camera new yaw angle
11660         float newYaw = this.selectedCamera.getYaw() + deltaYaw;
11661         this.selectedCamera.setYaw(newYaw);
11662 
11663         getView().setToolTipFeedback(getToolTipFeedbackText(newYaw), x, y);
11664 
11665         this.xLastMouseMove = x;
11666         this.yLastMouseMove = y;
11667         this.angleLastMouseMove = angleMouseMove;
11668       }
11669     }
11670 
11671     @Override
11672     public void releaseMouse(float x, float y) {
11673       setState(getSelectionState());
11674     }
11675 
11676     @Override
11677     public void escape() {
11678       this.selectedCamera.setYaw(this.oldYaw);
11679       setState(getSelectionState());
11680     }
11681 
11682     @Override
11683     public void exit() {
11684       PlanView planView = getView();
11685       planView.setResizeIndicatorVisible(false);
11686       planView.deleteFeedback();
11687       this.selectedCamera = null;
11688     }
11689 
11690     private String getToolTipFeedbackText(float angle) {
11691       return String.format(this.rotationToolTipFeedback,
11692           (Math.round(Math.toDegrees(angle)) + 360) % 360);
11693     }
11694   }
11695 
11696   /**
11697    * Camera pitch rotation state. This states manages the change of the observer camera pitch angle.
11698    */
11699   private class CameraPitchRotationState extends ControllerState {
11700     private ObserverCamera selectedCamera;
11701     private float          oldPitch;
11702     private String         rotationToolTipFeedback;
11703 
11704     @Override
11705     public Mode getMode() {
11706       return Mode.SELECTION;
11707     }
11708 
11709     @Override
11710     public boolean isModificationState() {
11711       return true;
11712     }
11713 
11714     @Override
11715     public void enter() {
11716       this.rotationToolTipFeedback = preferences.getLocalizedString(
11717           PlanController.class, "cameraPitchRotationToolTipFeedback");
11718       this.selectedCamera = (ObserverCamera)home.getSelectedItems().get(0);
11719       this.oldPitch = this.selectedCamera.getPitch();
11720       PlanView planView = getView();
11721       planView.setResizeIndicatorVisible(true);
11722       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldPitch),
11723           getXLastMousePress(), getYLastMousePress());
11724     }
11725 
11726     @Override
11727     public void moveMouse(float x, float y) {
11728       // Compute the new angle of the camera
11729       float newPitch = (float)(this.oldPitch
11730           + (y - getYLastMousePress()) * Math.cos(this.selectedCamera.getYaw()) * Math.PI / 360
11731           - (x - getXLastMousePress()) * Math.sin(this.selectedCamera.getYaw()) * Math.PI / 360);
11732       // Check new angle is between -90� and 90�
11733       newPitch = Math.max(newPitch, -(float)Math.PI / 2);
11734       newPitch = Math.min(newPitch, (float)Math.PI / 2);
11735 
11736       // Update camera pitch angle
11737       this.selectedCamera.setPitch(newPitch);
11738 
11739       getView().setToolTipFeedback(getToolTipFeedbackText(newPitch), x, y);
11740     }
11741 
11742     @Override
11743     public void releaseMouse(float x, float y) {
11744       setState(getSelectionState());
11745     }
11746 
11747     @Override
11748     public void escape() {
11749       this.selectedCamera.setPitch(this.oldPitch);
11750       setState(getSelectionState());
11751     }
11752 
11753     @Override
11754     public void exit() {
11755       PlanView planView = getView();
11756       planView.setResizeIndicatorVisible(false);
11757       planView.deleteFeedback();
11758       this.selectedCamera = null;
11759     }
11760 
11761     private String getToolTipFeedbackText(float angle) {
11762       return String.format(this.rotationToolTipFeedback,
11763           Math.round(Math.toDegrees(angle)) % 360);
11764     }
11765   }
11766 
11767   /**
11768    * Camera elevation state. This states manages the change of the observer camera elevation.
11769    */
11770   private class CameraElevationState extends ControllerState {
11771     private ObserverCamera selectedCamera;
11772     private float          oldElevation;
11773     private String         cameraElevationToolTipFeedback;
11774     private String         observerHeightToolTipFeedback;
11775 
11776     @Override
11777     public Mode getMode() {
11778       return Mode.SELECTION;
11779     }
11780 
11781     @Override
11782     public boolean isModificationState() {
11783       return true;
11784     }
11785 
11786     @Override
11787     public void enter() {
11788       this.cameraElevationToolTipFeedback = preferences.getLocalizedString(
11789           PlanController.class, "cameraElevationToolTipFeedback");
11790       this.observerHeightToolTipFeedback = preferences.getLocalizedString(
11791           PlanController.class, "observerHeightToolTipFeedback");
11792       this.selectedCamera = (ObserverCamera)home.getSelectedItems().get(0);
11793       this.oldElevation = this.selectedCamera.getZ();
11794       PlanView planView = getView();
11795       planView.setResizeIndicatorVisible(true);
11796       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldElevation),
11797           getXLastMousePress(), getYLastMousePress());
11798     }
11799 
11800     @Override
11801     public void moveMouse(float x, float y) {
11802       // Compute the new elevation of the camera
11803       float newElevation = (float)(this.oldElevation - (y - getYLastMousePress()));
11804       List<Level> levels = home.getLevels();
11805       float minimumElevation = levels.size() == 0  ? 10  : 10 + levels.get(0).getElevation();
11806       // Check new elevation is between min and max
11807       newElevation = Math.min(Math.max(newElevation, minimumElevation), preferences.getLengthUnit().getMaximumElevation());
11808 
11809       // Update camera elevation
11810       this.selectedCamera.setZ(newElevation);
11811       getView().setToolTipFeedback(getToolTipFeedbackText(newElevation), x, y);
11812     }
11813 
11814     @Override
11815     public void releaseMouse(float x, float y) {
11816       setState(getSelectionState());
11817     }
11818 
11819     @Override
11820     public void escape() {
11821       this.selectedCamera.setZ(this.oldElevation);
11822       setState(getSelectionState());
11823     }
11824 
11825     @Override
11826     public void exit() {
11827       PlanView planView = getView();
11828       planView.setResizeIndicatorVisible(false);
11829       planView.deleteFeedback();
11830       this.selectedCamera = null;
11831     }
11832 
11833     private String getToolTipFeedbackText(float elevation) {
11834       String toolTipFeedbackText = "<html>" + String.format(this.cameraElevationToolTipFeedback,
11835           preferences.getLengthUnit().getFormatWithUnit().format(elevation));
11836       if (!this.selectedCamera.isFixedSize() && elevation >= 70 && elevation <= 218.75f) {
11837         toolTipFeedbackText += "<br>" + String.format(this.observerHeightToolTipFeedback,
11838             preferences.getLengthUnit().getFormatWithUnit().format(elevation * 15 / 14));
11839       }
11840       return toolTipFeedbackText;
11841     }
11842   }
11843 
11844   /**
11845    * Dimension line creation state. This state manages transition to
11846    * other modes, and initial dimension line creation.
11847    */
11848   private class DimensionLineCreationState extends AbstractModeChangeState {
11849     private boolean magnetismEnabled;
11850 
11851     @Override
11852     public Mode getMode() {
11853       return Mode.DIMENSION_LINE_CREATION;
11854     }
11855 
11856     @Override
11857     public void enter() {
11858       getView().setCursor(PlanView.CursorType.DRAW);
11859       toggleMagnetism(wasMagnetismToggledLastMousePress());
11860     }
11861 
11862     @Override
11863     public void moveMouse(float x, float y) {
11864       getView().setAlignmentFeedback(DimensionLine.class, null, x, y, false);
11865       DimensionLine dimensionLine = getMeasuringDimensionLineAt(x, y, this.magnetismEnabled);
11866       if (dimensionLine != null) {
11867         getView().setDimensionLinesFeedback(Arrays.asList(new DimensionLine [] {dimensionLine}));
11868       } else {
11869         getView().setDimensionLinesFeedback(null);
11870       }
11871     }
11872 
11873     @Override
11874     public void pressMouse(float x, float y, int clickCount,
11875                            boolean shiftDown, boolean duplicationActivated) {
11876       // Ignore double clicks (may happen when state is activated returning from DimensionLineDrawingState)
11877       if (clickCount == 1) {
11878         // Change state to DimensionLineDrawingState
11879         setState(getDimensionLineDrawingState());
11880       }
11881     }
11882 
11883     @Override
11884     public void setEditionActivated(boolean editionActivated) {
11885       if (editionActivated) {
11886         setState(getDimensionLineDrawingState());
11887         PlanController.this.setEditionActivated(editionActivated);
11888       }
11889     }
11890 
11891     @Override
11892     public void toggleMagnetism(boolean magnetismToggled) {
11893       // Compute active magnetism
11894       this.magnetismEnabled = preferences.isMagnetismEnabled()
11895                               ^ magnetismToggled;
11896       if (getPointerTypeLastMousePress() != View.PointerType.TOUCH) {
11897         moveMouse(getXLastMouseMove(), getYLastMouseMove());
11898       }
11899     }
11900 
11901     @Override
11902     public void exit() {
11903       getView().deleteFeedback();
11904     }
11905   }
11906 
11907   /**
11908    * Dimension line drawing state. This state manages dimension line creation at mouse press.
11909    */
11910   private class DimensionLineDrawingState extends ControllerState {
11911     private float            xStart;
11912     private float            yStart;
11913     private boolean          editingStartPoint;
11914     private DimensionLine    newDimensionLine;
11915     private List<Selectable> oldSelection;
11916     private boolean          oldBasePlanLocked;
11917     private boolean          oldAllLevelsSelection;
11918     private boolean          magnetismEnabled;
11919     private boolean          alignmentActivated;
11920     private boolean          offsetChoice;
11921 
11922     @Override
11923     public Mode getMode() {
11924       return Mode.DIMENSION_LINE_CREATION;
11925     }
11926 
11927     @Override
11928     public boolean isModificationState() {
11929       return true;
11930     }
11931 
11932     @Override
11933     public boolean isBasePlanModificationState() {
11934       return true;
11935     }
11936 
11937     @Override
11938     public void setMode(Mode mode) {
11939       // Escape current creation and change state to matching mode
11940       escape();
11941       if (mode == Mode.SELECTION) {
11942         setState(getSelectionState());
11943       } else if (mode == Mode.PANNING) {
11944         setState(getPanningState());
11945       } else if (mode == Mode.WALL_CREATION) {
11946         setState(getWallCreationState());
11947       } else if (mode == Mode.ROOM_CREATION) {
11948         setState(getRoomCreationState());
11949       } else if (mode == Mode.POLYLINE_CREATION) {
11950         setState(getPolylineCreationState());
11951       } else if (mode == Mode.LABEL_CREATION) {
11952         setState(getLabelCreationState());
11953       }
11954     }
11955 
11956     @Override
11957     public void enter() {
11958       this.oldSelection = home.getSelectedItems();
11959       this.oldBasePlanLocked = home.isBasePlanLocked();
11960       this.oldAllLevelsSelection = home.isAllLevelsSelection();
11961       this.xStart = getXLastMouseMove();
11962       this.yStart = getYLastMouseMove();
11963       this.editingStartPoint = false;
11964       this.offsetChoice = false;
11965       this.newDimensionLine = null;
11966       deselectAll();
11967       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
11968       toggleMagnetism(wasMagnetismToggledLastMousePress());
11969       DimensionLine dimensionLine = getMeasuringDimensionLineAt(
11970           getXLastMousePress(), getYLastMousePress(), this.magnetismEnabled);
11971       if (dimensionLine != null) {
11972         getView().setDimensionLinesFeedback(Arrays.asList(new DimensionLine [] {dimensionLine}));
11973       }
11974       getView().setAlignmentFeedback(DimensionLine.class,
11975           null, getXLastMousePress(), getYLastMousePress(), false);
11976     }
11977 
11978     @Override
11979     public void moveMouse(float x, float y) {
11980       PlanView planView = getView();
11981       planView.deleteFeedback();
11982       if (this.offsetChoice) {
11983           float distanceToDimensionLine = (float)Line2D.ptLineDist(
11984               this.newDimensionLine.getXStart(), this.newDimensionLine.getYStart(),
11985               this.newDimensionLine.getXEnd(), this.newDimensionLine.getYEnd(), x, y);
11986           if (this.newDimensionLine.getLength() > 0) {
11987             int relativeCCW = Line2D.relativeCCW(
11988                 this.newDimensionLine.getXStart(), this.newDimensionLine.getYStart(),
11989                 this.newDimensionLine.getXEnd(), this.newDimensionLine.getYEnd(), x, y);
11990             this.newDimensionLine.setOffset(
11991                 -Math.signum(relativeCCW) * distanceToDimensionLine);
11992           }
11993       } else {
11994         // Compute the coordinates where dimension line end point should be moved
11995         float newX;
11996         float newY;
11997         if (this.magnetismEnabled
11998             || this.alignmentActivated) {
11999           PointWithAngleMagnetism point = new PointWithAngleMagnetism(
12000               this.xStart, this.yStart, x, y,
12001               preferences.getLengthUnit(), planView.getPixelLength());
12002           newX = point.getX();
12003           newY = point.getY();
12004         } else {
12005           newX = x;
12006           newY = y;
12007         }
12008 
12009         // If current dimension line doesn't exist
12010         if (this.newDimensionLine == null) {
12011           // Create a new one
12012           this.newDimensionLine = createDimensionLine(this.xStart, this.yStart, newX, newY, 0);
12013           getView().setDimensionLinesFeedback(null);
12014         } else {
12015           // Otherwise update its end points
12016           if (this.editingStartPoint) {
12017             this.newDimensionLine.setXStart(newX);
12018             this.newDimensionLine.setYStart(newY);
12019           } else {
12020             this.newDimensionLine.setXEnd(newX);
12021             this.newDimensionLine.setYEnd(newY);
12022           }
12023         }
12024         updateReversedDimensionLine();
12025 
12026         planView.setAlignmentFeedback(DimensionLine.class,
12027             this.newDimensionLine, newX, newY, false);
12028       }
12029       // Ensure point at (x,y) is visible
12030       planView.makePointVisible(x, y);
12031     }
12032 
12033     /**
12034      * Swaps start and end point of the created dimension line if needed
12035      * to ensure its text is never upside down.
12036      */
12037     private void updateReversedDimensionLine() {
12038       double angle = getDimensionLineAngle();
12039       boolean reverse = angle < -Math.PI / 2 || angle > Math.PI / 2;
12040       if (reverse ^ this.editingStartPoint) {
12041         reverseDimensionLine(this.newDimensionLine);
12042         this.editingStartPoint = !this.editingStartPoint;
12043       }
12044     }
12045 
12046     private double getDimensionLineAngle() {
12047       if (this.newDimensionLine.getLength() == 0) {
12048         return 0;
12049       } else {
12050         if (this.editingStartPoint) {
12051           return Math.atan2(this.yStart - this.newDimensionLine.getYStart(),
12052               this.newDimensionLine.getXStart() - this.xStart);
12053         } else {
12054           return Math.atan2(this.yStart - this.newDimensionLine.getYEnd(),
12055               this.newDimensionLine.getXEnd() - this.xStart);
12056         }
12057       }
12058     }
12059 
12060     @Override
12061     public void pressMouse(float x, float y, int clickCount,
12062                            boolean shiftDown, boolean duplicationActivated) {
12063       if (this.newDimensionLine == null
12064           && clickCount == 2) {
12065         // Try to guess the item to measure
12066         DimensionLine dimensionLine = getMeasuringDimensionLineAt(x, y, this.magnetismEnabled);
12067         if (dimensionLine != null) {
12068           this.newDimensionLine = createDimensionLine(
12069               dimensionLine.getXStart(), dimensionLine.getYStart(),
12070               dimensionLine.getXEnd(), dimensionLine.getYEnd(),
12071               dimensionLine.getOffset());
12072           if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
12073             validateDrawnDimensionLine();
12074           }
12075         } else {
12076           setState(getDimensionLineCreationState());
12077           return;
12078         }
12079       }
12080       // Create a new dimension line only when it will have a length > 0
12081       // meaning after the first mouse move
12082       if (this.newDimensionLine != null) {
12083         if (this.offsetChoice) {
12084           validateDrawnDimensionLine();
12085         } else {
12086           // Switch to offset choice
12087           this.offsetChoice = true;
12088           PlanView planView = getView();
12089           planView.setCursor(PlanView.CursorType.HEIGHT);
12090           planView.deleteFeedback();
12091         }
12092       }
12093     }
12094 
12095     @Override
12096     public void releaseMouse(float x, float y) {
12097       if (getPointerTypeLastMousePress() == View.PointerType.TOUCH
12098           && this.newDimensionLine != null) {
12099         validateDrawnDimensionLine();
12100       }
12101     }
12102 
12103     private void validateDrawnDimensionLine() {
12104       selectItem(this.newDimensionLine);
12105       // Post dimension line creation to undo support
12106       postCreateDimensionLines(Arrays.asList(new DimensionLine [] {this.newDimensionLine}),
12107           this.oldSelection, this.oldBasePlanLocked, this.oldAllLevelsSelection);
12108       this.newDimensionLine = null;
12109       if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
12110         setState(getSelectionState());
12111       } else {
12112         // Change state to DimensionLineCreationState
12113         setState(getDimensionLineCreationState());
12114       }
12115     }
12116 
12117     @Override
12118     public void setEditionActivated(boolean editionActivated) {
12119       PlanView planView = getView();
12120       if (editionActivated) {
12121         planView.deleteFeedback();
12122         if (this.newDimensionLine == null) {
12123           // Edit xStart and yStart
12124           planView.setToolTipEditedProperties(new EditableProperty [] {EditableProperty.X,
12125                                                                        EditableProperty.Y},
12126               new Object [] {this.xStart, this.yStart},
12127               this.xStart, this.yStart);
12128         } else if (this.offsetChoice) {
12129           // Edit offset
12130           planView.setToolTipEditedProperties(new EditableProperty [] {EditableProperty.OFFSET},
12131               new Object [] {this.newDimensionLine.getOffset()},
12132               this.newDimensionLine.getXEnd(), this.newDimensionLine.getYEnd());
12133         } else {
12134           // Edit length and angle
12135           planView.setToolTipEditedProperties(new EditableProperty [] {EditableProperty.LENGTH,
12136                                                                        EditableProperty.ANGLE},
12137               new Object [] {this.newDimensionLine.getLength(),
12138                              (int)Math.round(Math.toDegrees(getDimensionLineAngle()))},
12139               this.newDimensionLine.getXEnd(), this.newDimensionLine.getYEnd());
12140         }
12141       } else {
12142         if (this.newDimensionLine == null) {
12143           // Create a new dimension line once user entered its start point
12144           LengthUnit lengthUnit = preferences.getLengthUnit();
12145           float defaultLength = lengthUnit == LengthUnit.INCH || lengthUnit == LengthUnit.INCH_DECIMALS
12146               ? LengthUnit.footToCentimeter(3) : 100;
12147           this.newDimensionLine = createDimensionLine(this.xStart, this.yStart,
12148               this.xStart + defaultLength, this.yStart, 0);
12149           // Activate automatically second step to let user enter the
12150           // length and angle of the new dimension line
12151           planView.deleteFeedback();
12152           setEditionActivated(true);
12153         } else if (this.offsetChoice) {
12154           validateDrawnDimensionLine();
12155         } else {
12156           this.offsetChoice = true;
12157           setEditionActivated(true);
12158         }
12159       }
12160     }
12161 
12162     @Override
12163     public void updateEditableProperty(EditableProperty editableProperty, Object value) {
12164       PlanView planView = getView();
12165       float maximumLength = preferences.getLengthUnit().getMaximumLength();
12166       if (this.newDimensionLine == null) {
12167         // Update start point of the dimension line
12168         switch (editableProperty) {
12169           case X :
12170             this.xStart = value != null ? ((Number)value).floatValue() : 0;
12171             this.xStart = Math.max(-maximumLength, Math.min(this.xStart, maximumLength));
12172             break;
12173           case Y :
12174             this.yStart = value != null ? ((Number)value).floatValue() : 0;
12175             this.yStart = Math.max(-maximumLength, Math.min(this.yStart, maximumLength));
12176             break;
12177         }
12178         planView.setAlignmentFeedback(DimensionLine.class, null, this.xStart, this.yStart, true);
12179         planView.makePointVisible(this.xStart, this.yStart);
12180       } else if (this.offsetChoice) {
12181         if (editableProperty == EditableProperty.OFFSET) {
12182           // Update new dimension line offset
12183           float offset = value != null ? ((Number)value).floatValue() : 0;
12184           offset = Math.max(-maximumLength, Math.min(offset, maximumLength));
12185           this.newDimensionLine.setOffset(offset);
12186         }
12187       } else {
12188         float newX;
12189         float newY;
12190         // Update end point of the dimension line
12191         switch (editableProperty) {
12192           case LENGTH :
12193             float length = value != null ? ((Number)value).floatValue() : 0;
12194             length = Math.max(0.001f, Math.min(length, maximumLength));
12195             double dimensionLineAngle = getDimensionLineAngle();
12196             newX = (float)(this.xStart + length * Math.cos(dimensionLineAngle));
12197             newY = (float)(this.yStart - length * Math.sin(dimensionLineAngle));
12198             break;
12199           case ANGLE :
12200             dimensionLineAngle = Math.toRadians(value != null ? ((Number)value).floatValue() : 0);
12201             float dimensionLineLength = this.newDimensionLine.getLength();
12202             newX = (float)(this.xStart + dimensionLineLength * Math.cos(dimensionLineAngle));
12203             newY = (float)(this.yStart - dimensionLineLength * Math.sin(dimensionLineAngle));
12204             break;
12205           default :
12206             return;
12207         }
12208 
12209         // Update new dimension line
12210         if (this.editingStartPoint) {
12211           this.newDimensionLine.setXStart(newX);
12212           this.newDimensionLine.setYStart(newY);
12213         } else {
12214           this.newDimensionLine.setXEnd(newX);
12215           this.newDimensionLine.setYEnd(newY);
12216         }
12217         updateReversedDimensionLine();
12218         planView.setAlignmentFeedback(DimensionLine.class, this.newDimensionLine, newX, newY, false);
12219         // Ensure dimension line end points are visible
12220         planView.makePointVisible(this.xStart, this.yStart);
12221         planView.makePointVisible(newX, newY);
12222       }
12223     }
12224 
12225     @Override
12226     public void toggleMagnetism(boolean magnetismToggled) {
12227       // Compute active magnetism
12228       this.magnetismEnabled = preferences.isMagnetismEnabled()
12229                               ^ magnetismToggled;
12230       // If the new dimension line already exists,
12231       // compute again its end as if mouse moved
12232       if (this.newDimensionLine != null && !this.offsetChoice) {
12233         moveMouse(getXLastMouseMove(), getYLastMouseMove());
12234       }
12235     }
12236 
12237     @Override
12238     public void setAlignmentActivated(boolean alignmentActivated) {
12239       this.alignmentActivated = alignmentActivated;
12240       moveMouse(getXLastMouseMove(), getYLastMouseMove());
12241     }
12242 
12243     @Override
12244     public void escape() {
12245       if (this.newDimensionLine != null) {
12246         // Delete current created dimension line
12247         home.deleteDimensionLine(this.newDimensionLine);
12248       }
12249       if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
12250         setState(getSelectionState());
12251       } else {
12252         // Change state to DimensionLineCreationState
12253         setState(getDimensionLineCreationState());
12254       }
12255     }
12256 
12257     @Override
12258     public void exit() {
12259       getView().deleteFeedback();
12260       this.newDimensionLine = null;
12261       this.oldSelection = null;
12262     }
12263   }
12264 
12265   /**
12266    * Dimension line resize state. This state manages dimension line resizing.
12267    */
12268   private class DimensionLineResizeState extends ControllerState {
12269     private DimensionLine selectedDimensionLine;
12270     private boolean       editingStartPoint;
12271     private float         oldX;
12272     private float         oldY;
12273     private boolean       reversedDimensionLine;
12274     private float         deltaXToResizePoint;
12275     private float         deltaYToResizePoint;
12276     private float         distanceFromResizePointToDimensionBaseLine;
12277     private boolean       magnetismEnabled;
12278     private boolean       alignmentActivated;
12279 
12280 
12281     @Override
12282     public Mode getMode() {
12283       return Mode.SELECTION;
12284     }
12285 
12286     @Override
12287     public boolean isModificationState() {
12288       return true;
12289     }
12290 
12291     @Override
12292     public boolean isBasePlanModificationState() {
12293       return true;
12294     }
12295 
12296     @Override
12297     public void enter() {
12298       PlanView planView = getView();
12299       planView.setResizeIndicatorVisible(true);
12300 
12301       this.selectedDimensionLine = (DimensionLine)home.getSelectedItems().get(0);
12302       this.editingStartPoint = this.selectedDimensionLine
12303           == getResizedDimensionLineStartAt(getXLastMousePress(), getYLastMousePress());
12304       if (this.editingStartPoint) {
12305         this.oldX = this.selectedDimensionLine.getXStart();
12306         this.oldY = this.selectedDimensionLine.getYStart();
12307       } else {
12308         this.oldX = this.selectedDimensionLine.getXEnd();
12309         this.oldY = this.selectedDimensionLine.getYEnd();
12310       }
12311       this.reversedDimensionLine = false;
12312 
12313       float xResizePoint;
12314       float yResizePoint;
12315       // Compute the closest resize point placed on the extension line and the distance
12316       // between that point and the dimension line base
12317       float alpha1 = (float)(this.selectedDimensionLine.getYEnd() - this.selectedDimensionLine.getYStart())
12318           / (this.selectedDimensionLine.getXEnd() - this.selectedDimensionLine.getXStart());
12319       // If line is vertical
12320       if (Math.abs(alpha1) > 1E5) {
12321         xResizePoint = getXLastMousePress();
12322         if (this.editingStartPoint) {
12323           yResizePoint = this.selectedDimensionLine.getYStart();
12324         } else {
12325           yResizePoint = this.selectedDimensionLine.getYEnd();
12326         }
12327       } else if (this.selectedDimensionLine.getYStart() == this.selectedDimensionLine.getYEnd()) {
12328         if (this.editingStartPoint) {
12329           xResizePoint = this.selectedDimensionLine.getXStart();
12330         } else {
12331           xResizePoint = this.selectedDimensionLine.getXEnd();
12332         }
12333         yResizePoint = getYLastMousePress();
12334       } else {
12335         float beta1 = getYLastMousePress() - alpha1 * getXLastMousePress();
12336         float alpha2 = -1 / alpha1;
12337         float beta2;
12338 
12339         if (this.editingStartPoint) {
12340           beta2 = this.selectedDimensionLine.getYStart() - alpha2 * this.selectedDimensionLine.getXStart();
12341         } else {
12342           beta2 = this.selectedDimensionLine.getYEnd() - alpha2 * this.selectedDimensionLine.getXEnd();
12343         }
12344         xResizePoint = (beta2 - beta1) / (alpha1 - alpha2);
12345         yResizePoint = alpha1 * xResizePoint + beta1;
12346       }
12347 
12348       this.deltaXToResizePoint = getXLastMousePress() - xResizePoint;
12349       this.deltaYToResizePoint = getYLastMousePress() - yResizePoint;
12350       if (this.editingStartPoint) {
12351         this.distanceFromResizePointToDimensionBaseLine = (float)Point2D.distance(xResizePoint, yResizePoint,
12352             this.selectedDimensionLine.getXStart(), this.selectedDimensionLine.getYStart());
12353         planView.setAlignmentFeedback(DimensionLine.class, this.selectedDimensionLine,
12354             this.selectedDimensionLine.getXStart(), this.selectedDimensionLine.getYStart(), false);
12355       } else {
12356         this.distanceFromResizePointToDimensionBaseLine = (float)Point2D.distance(xResizePoint, yResizePoint,
12357             this.selectedDimensionLine.getXEnd(), this.selectedDimensionLine.getYEnd());
12358         planView.setAlignmentFeedback(DimensionLine.class, this.selectedDimensionLine,
12359             this.selectedDimensionLine.getXEnd(), this.selectedDimensionLine.getYEnd(), false);
12360       }
12361       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
12362       toggleMagnetism(wasMagnetismToggledLastMousePress());
12363     }
12364 
12365     @Override
12366     public void moveMouse(float x, float y) {
12367       PlanView planView = getView();
12368       float xResizePoint = x - this.deltaXToResizePoint;
12369       float yResizePoint = y - this.deltaYToResizePoint;
12370       if (this.editingStartPoint) {
12371         // Compute the new start point of the dimension line knowing that the distance
12372         // from resize point to dimension line base is constant,
12373         // and that the end point of the dimension line doesn't move
12374         double distanceFromResizePointToDimensionLineEnd = Point2D.distance(xResizePoint, yResizePoint,
12375             this.selectedDimensionLine.getXEnd(), this.selectedDimensionLine.getYEnd());
12376         double distanceFromDimensionLineStartToDimensionLineEnd = Math.sqrt(
12377             distanceFromResizePointToDimensionLineEnd * distanceFromResizePointToDimensionLineEnd
12378             - this.distanceFromResizePointToDimensionBaseLine * this.distanceFromResizePointToDimensionBaseLine);
12379         if (distanceFromDimensionLineStartToDimensionLineEnd > 0) {
12380           double dimensionLineRelativeAngle = -Math.atan2(this.distanceFromResizePointToDimensionBaseLine,
12381               distanceFromDimensionLineStartToDimensionLineEnd);
12382           if (this.selectedDimensionLine.getOffset() >= 0) {
12383             dimensionLineRelativeAngle = -dimensionLineRelativeAngle;
12384           }
12385           double resizePointToDimensionLineEndAngle = Math.atan2(yResizePoint - this.selectedDimensionLine.getYEnd(),
12386               xResizePoint - this.selectedDimensionLine.getXEnd());
12387           double dimensionLineStartToDimensionLineEndAngle = dimensionLineRelativeAngle + resizePointToDimensionLineEndAngle;
12388           float xNewStartPoint = this.selectedDimensionLine.getXEnd() + (float)(distanceFromDimensionLineStartToDimensionLineEnd
12389               * Math.cos(dimensionLineStartToDimensionLineEndAngle));
12390           float yNewStartPoint = this.selectedDimensionLine.getYEnd() + (float)(distanceFromDimensionLineStartToDimensionLineEnd
12391               * Math.sin(dimensionLineStartToDimensionLineEndAngle));
12392 
12393           if (this.alignmentActivated
12394               || this.magnetismEnabled) {
12395             PointWithAngleMagnetism point = new PointWithAngleMagnetism(
12396                 this.selectedDimensionLine.getXEnd(),
12397                 this.selectedDimensionLine.getYEnd(), xNewStartPoint, yNewStartPoint,
12398                 preferences.getLengthUnit(), planView.getPixelLength());
12399             xNewStartPoint = point.getX();
12400             yNewStartPoint = point.getY();
12401           }
12402 
12403           moveDimensionLinePoint(this.selectedDimensionLine, xNewStartPoint, yNewStartPoint, this.editingStartPoint);
12404           updateReversedDimensionLine();
12405           planView.setAlignmentFeedback(DimensionLine.class, this.selectedDimensionLine,
12406               xNewStartPoint, yNewStartPoint, false);
12407         } else {
12408           planView.deleteFeedback();
12409         }
12410       } else {
12411         // Compute the new end point of the dimension line knowing that the distance
12412         // from resize point to dimension line base is constant,
12413         // and that the start point of the dimension line doesn't move
12414         double distanceFromResizePointToDimensionLineStart = Point2D.distance(xResizePoint, yResizePoint,
12415             this.selectedDimensionLine.getXStart(), this.selectedDimensionLine.getYStart());
12416         double distanceFromDimensionLineStartToDimensionLineEnd = Math.sqrt(
12417             distanceFromResizePointToDimensionLineStart * distanceFromResizePointToDimensionLineStart
12418             - this.distanceFromResizePointToDimensionBaseLine * this.distanceFromResizePointToDimensionBaseLine);
12419         if (distanceFromDimensionLineStartToDimensionLineEnd > 0) {
12420           double dimensionLineRelativeAngle = Math.atan2(this.distanceFromResizePointToDimensionBaseLine,
12421               distanceFromDimensionLineStartToDimensionLineEnd);
12422           if (this.selectedDimensionLine.getOffset() >= 0) {
12423             dimensionLineRelativeAngle = -dimensionLineRelativeAngle;
12424           }
12425           double resizePointToDimensionLineStartAngle = Math.atan2(yResizePoint - this.selectedDimensionLine.getYStart(),
12426               xResizePoint - this.selectedDimensionLine.getXStart());
12427           double dimensionLineStartToDimensionLineEndAngle = dimensionLineRelativeAngle + resizePointToDimensionLineStartAngle;
12428           float xNewEndPoint = this.selectedDimensionLine.getXStart() + (float)(distanceFromDimensionLineStartToDimensionLineEnd
12429               * Math.cos(dimensionLineStartToDimensionLineEndAngle));
12430           float yNewEndPoint = this.selectedDimensionLine.getYStart() + (float)(distanceFromDimensionLineStartToDimensionLineEnd
12431               * Math.sin(dimensionLineStartToDimensionLineEndAngle));
12432 
12433           if (this.alignmentActivated
12434               || this.magnetismEnabled) {
12435             PointWithAngleMagnetism point = new PointWithAngleMagnetism(
12436                 this.selectedDimensionLine.getXStart(),
12437                 this.selectedDimensionLine.getYStart(), xNewEndPoint, yNewEndPoint,
12438                 preferences.getLengthUnit(), planView.getPixelLength());
12439             xNewEndPoint = point.getX();
12440             yNewEndPoint = point.getY();
12441           }
12442 
12443           moveDimensionLinePoint(this.selectedDimensionLine, xNewEndPoint, yNewEndPoint, this.editingStartPoint);
12444           updateReversedDimensionLine();
12445           planView.setAlignmentFeedback(DimensionLine.class, this.selectedDimensionLine,
12446               xNewEndPoint, yNewEndPoint, false);
12447         } else {
12448           planView.deleteFeedback();
12449         }
12450       }
12451 
12452       // Ensure point at (x,y) is visible
12453       getView().makePointVisible(x, y);
12454     }
12455 
12456     /**
12457      * Swaps start and end point of the dimension line if needed
12458      * to ensure its text is never upside down.
12459      */
12460     private void updateReversedDimensionLine() {
12461       double angle = getDimensionLineAngle();
12462       if (angle < -Math.PI / 2 || angle > Math.PI / 2) {
12463         reverseDimensionLine(this.selectedDimensionLine);
12464         this.editingStartPoint = !this.editingStartPoint;
12465         this.reversedDimensionLine = !this.reversedDimensionLine;
12466       }
12467     }
12468 
12469     private double getDimensionLineAngle() {
12470       if (this.selectedDimensionLine.getLength() == 0) {
12471         return 0;
12472       } else {
12473         return Math.atan2(this.selectedDimensionLine.getYStart() - this.selectedDimensionLine.getYEnd(),
12474             this.selectedDimensionLine.getXEnd() - this.selectedDimensionLine.getXStart());
12475       }
12476     }
12477 
12478     @Override
12479     public void releaseMouse(float x, float y) {
12480       postDimensionLineResize(this.selectedDimensionLine, this.oldX, this.oldY,
12481           this.editingStartPoint, this.reversedDimensionLine);
12482       setState(getSelectionState());
12483     }
12484 
12485     @Override
12486     public void toggleMagnetism(boolean magnetismToggled) {
12487       // Compute active magnetism
12488       this.magnetismEnabled = preferences.isMagnetismEnabled()
12489                               ^ magnetismToggled;
12490       moveMouse(getXLastMouseMove(), getYLastMouseMove());
12491     }
12492 
12493     @Override
12494     public void setAlignmentActivated(boolean alignmentActivated) {
12495       this.alignmentActivated = alignmentActivated;
12496       moveMouse(getXLastMouseMove(), getYLastMouseMove());
12497     }
12498 
12499     @Override
12500     public void escape() {
12501       if (this.reversedDimensionLine) {
12502         reverseDimensionLine(this.selectedDimensionLine);
12503         this.editingStartPoint = !this.editingStartPoint;
12504       }
12505       moveDimensionLinePoint(this.selectedDimensionLine, this.oldX, this.oldY, this.editingStartPoint);
12506       setState(getSelectionState());
12507     }
12508 
12509     @Override
12510     public void exit() {
12511       PlanView planView = getView();
12512       planView.deleteFeedback();
12513       planView.setResizeIndicatorVisible(false);
12514       this.selectedDimensionLine = null;
12515     }
12516   }
12517 
12518   /**
12519    * Dimension line offset state. This state manages dimension line offset.
12520    */
12521   private class DimensionLineOffsetState extends ControllerState {
12522     private DimensionLine selectedDimensionLine;
12523     private float         oldOffset;
12524     private float         deltaXToOffsetPoint;
12525     private float         deltaYToOffsetPoint;
12526 
12527     @Override
12528     public Mode getMode() {
12529       return Mode.SELECTION;
12530     }
12531 
12532     @Override
12533     public boolean isModificationState() {
12534       return true;
12535     }
12536 
12537     @Override
12538     public boolean isBasePlanModificationState() {
12539       return true;
12540     }
12541 
12542     @Override
12543     public void enter() {
12544       this.selectedDimensionLine = (DimensionLine)home.getSelectedItems().get(0);
12545       this.oldOffset = this.selectedDimensionLine.getOffset();
12546       double angle = Math.atan2(this.selectedDimensionLine.getYEnd() - this.selectedDimensionLine.getYStart(),
12547           this.selectedDimensionLine.getXEnd() - this.selectedDimensionLine.getXStart());
12548       float dx = (float)-Math.sin(angle) * this.oldOffset;
12549       float dy = (float)Math.cos(angle) * this.oldOffset;
12550       float xMiddle = (this.selectedDimensionLine.getXStart() + this.selectedDimensionLine.getXEnd()) / 2 + dx;
12551       float yMiddle = (this.selectedDimensionLine.getYStart() + this.selectedDimensionLine.getYEnd()) / 2 + dy;
12552       this.deltaXToOffsetPoint = getXLastMousePress() - xMiddle;
12553       this.deltaYToOffsetPoint = getYLastMousePress() - yMiddle;
12554       PlanView planView = getView();
12555       planView.setResizeIndicatorVisible(true);
12556     }
12557 
12558     @Override
12559     public void moveMouse(float x, float y) {
12560       float newX = x - this.deltaXToOffsetPoint;
12561       float newY = y - this.deltaYToOffsetPoint;
12562 
12563       float distanceToDimensionLine =
12564           (float)Line2D.ptLineDist(this.selectedDimensionLine.getXStart(), this.selectedDimensionLine.getYStart(),
12565               this.selectedDimensionLine.getXEnd(), this.selectedDimensionLine.getYEnd(), newX, newY);
12566       int relativeCCW = Line2D.relativeCCW(this.selectedDimensionLine.getXStart(), this.selectedDimensionLine.getYStart(),
12567           this.selectedDimensionLine.getXEnd(), this.selectedDimensionLine.getYEnd(), newX, newY);
12568       this.selectedDimensionLine.setOffset(
12569            -Math.signum(relativeCCW) * distanceToDimensionLine);
12570 
12571       // Ensure point at (x,y) is visible
12572       getView().makePointVisible(x, y);
12573     }
12574 
12575     @Override
12576     public void releaseMouse(float x, float y) {
12577       postDimensionLineOffset(this.selectedDimensionLine, this.oldOffset);
12578       setState(getSelectionState());
12579     }
12580 
12581     @Override
12582     public void escape() {
12583       this.selectedDimensionLine.setOffset(this.oldOffset);
12584       setState(getSelectionState());
12585     }
12586 
12587     @Override
12588     public void exit() {
12589       getView().setResizeIndicatorVisible(false);
12590       this.selectedDimensionLine = null;
12591     }
12592   }
12593 
12594   /**
12595    * Room creation state. This state manages transition to
12596    * other modes, and initial room creation.
12597    */
12598   private class RoomCreationState extends AbstractModeChangeState {
12599     private boolean magnetismEnabled;
12600 
12601     @Override
12602     public Mode getMode() {
12603       return Mode.ROOM_CREATION;
12604     }
12605 
12606     @Override
12607     public void enter() {
12608       getView().setCursor(PlanView.CursorType.DRAW);
12609       toggleMagnetism(wasMagnetismToggledLastMousePress());
12610     }
12611 
12612     @Override
12613     public void moveMouse(float x, float y) {
12614       if (this.magnetismEnabled) {
12615         // Find the closest wall or room point to current mouse location
12616         PointMagnetizedToClosestWallOrRoomPoint point = new PointMagnetizedToClosestWallOrRoomPoint(x, y);
12617         if (point.isMagnetized()) {
12618           getView().setAlignmentFeedback(Room.class, null, point.getX(),
12619               point.getY(), point.isMagnetized());
12620         } else {
12621           RoomPointWithAngleMagnetism pointWithAngleMagnetism = new RoomPointWithAngleMagnetism(null, -1,
12622               getXLastMouseMove(), getYLastMouseMove(), getXLastMouseMove(), getYLastMouseMove());
12623           getView().setAlignmentFeedback(Room.class, null, pointWithAngleMagnetism.getX(),
12624               pointWithAngleMagnetism.getY(), point.isMagnetized());
12625         }
12626       } else {
12627         getView().setAlignmentFeedback(Room.class, null, x, y, false);
12628       }
12629     }
12630 
12631     @Override
12632     public void pressMouse(float x, float y, int clickCount,
12633                            boolean shiftDown, boolean duplicationActivated) {
12634       // Change state to RoomDrawingState
12635       setState(getRoomDrawingState());
12636     }
12637 
12638     @Override
12639     public void setEditionActivated(boolean editionActivated) {
12640       if (editionActivated) {
12641         setState(getRoomDrawingState());
12642         PlanController.this.setEditionActivated(editionActivated);
12643       }
12644     }
12645 
12646     @Override
12647     public void toggleMagnetism(boolean magnetismToggled) {
12648       // Compute active magnetism
12649       this.magnetismEnabled = preferences.isMagnetismEnabled()
12650                               ^ magnetismToggled;
12651       if (getPointerTypeLastMousePress() != View.PointerType.TOUCH) {
12652         // Compute again feedback point as if mouse moved
12653         moveMouse(getXLastMouseMove(), getYLastMouseMove());
12654       }
12655     }
12656 
12657     @Override
12658     public void exit() {
12659       getView().deleteFeedback();
12660     }
12661   }
12662 
12663   /**
12664    * Room modification state.
12665    */
12666   private abstract class AbstractRoomState extends ControllerState {
12667     private String roomSideLengthToolTipFeedback;
12668     private String roomSideAngleToolTipFeedback;
12669 
12670     @Override
12671     public void enter() {
12672       this.roomSideLengthToolTipFeedback = preferences.getLocalizedString(
12673           PlanController.class, "roomSideLengthToolTipFeedback");
12674       try {
12675         this.roomSideAngleToolTipFeedback = preferences.getLocalizedString(
12676             PlanController.class, "roomSideAngleToolTipFeedback");
12677       } catch (IllegalArgumentException ex) {
12678         // This tool tip part is optional
12679       }
12680     }
12681 
12682     protected String getToolTipFeedbackText(Room room, int pointIndex) {
12683       float length = getRoomSideLength(room, pointIndex);
12684       int angle = getRoomSideAngle(room, pointIndex);
12685       String toolTipFeedbackText = "<html>" + String.format(this.roomSideLengthToolTipFeedback, preferences.getLengthUnit().getFormatWithUnit().format(length));
12686       if (this.roomSideAngleToolTipFeedback != null
12687           && this.roomSideAngleToolTipFeedback.length() > 0) {
12688         toolTipFeedbackText += "<br>" + String.format(this.roomSideAngleToolTipFeedback, angle);
12689       }
12690       return toolTipFeedbackText;
12691     }
12692 
12693     protected float getRoomSideLength(Room room, int pointIndex) {
12694       float [][] points = room.getPoints();
12695       float [] previousPoint = points [(pointIndex + points.length - 1) % points.length];
12696       return (float)Point2D.distance(previousPoint [0], previousPoint [1],
12697           points [pointIndex][0], points [pointIndex][1]);
12698     }
12699 
12700     protected List<DimensionLine> getTriangulationDimensionLines(Room room, int pointIndex) {
12701       float [][] points = room.getPoints();
12702       float [] previousPoint = points [(pointIndex + points.length - 1) % points.length];
12703       List<DimensionLine> dimensionLines = new ArrayList<DimensionLine>(3);
12704       float offset = 20 / getView().getScale();
12705       if (!Arrays.equals(points [pointIndex], previousPoint)) {
12706         dimensionLines.add(getDimensionLineBetweenPoints(previousPoint, points [pointIndex],
12707             isDimensionInsideRoom(room, previousPoint, points [pointIndex]) ? -offset : offset, false));
12708       }
12709       if (points.length > 2) {
12710         float [] nextPoint = points [pointIndex + 1 < points.length ? pointIndex + 1 : 0];
12711         if (!Arrays.equals(points [pointIndex], nextPoint)) {
12712           dimensionLines.add(getDimensionLineBetweenPoints(points [pointIndex], nextPoint,
12713               isDimensionInsideRoom(room, points [pointIndex], nextPoint) ? -offset : offset, false));
12714         }
12715         float [] previousPreviousPoint = points [(pointIndex + points.length - 2) % points.length];
12716         if (points.length == 3) {
12717           dimensionLines.add(getDimensionLineBetweenPoints(previousPoint, previousPreviousPoint,
12718               isDimensionInsideRoom(room, previousPoint, previousPreviousPoint) ? -offset : offset, false));
12719         } else if (!Arrays.equals(points [pointIndex], previousPreviousPoint)) {
12720           dimensionLines.add(getDimensionLineBetweenPoints(points [pointIndex], previousPreviousPoint, 0, false));
12721         }
12722       }
12723       return dimensionLines;
12724     }
12725 
12726     private boolean isDimensionInsideRoom(Room room, float [] point1, float [] point2) {
12727       // Search the coordinates of the point where the dimension will be displayed
12728       AffineTransform rotation = AffineTransform.getRotateInstance(
12729           Math.atan2(point2 [1] - point1 [1], point2 [0] - point1 [0]), point1 [0], point1 [1]);
12730       float [] dimensionPoint = {(float)(point1 [0] + Point2D.distance(point1 [0], point1 [1], point2 [0], point2 [1]) / 2), point1 [1] + 1f};
12731       rotation.transform(dimensionPoint, 0, dimensionPoint, 0, 1);
12732       return room.containsPoint(dimensionPoint [0], dimensionPoint [1], 0);
12733     }
12734 
12735     /**
12736      * Returns room side angle at the given point index in degrees.
12737      */
12738     protected Integer getRoomSideAngle(Room room, int pointIndex) {
12739       float [][] points = room.getPoints();
12740       float [] point = points [pointIndex];
12741       float [] previousPoint = points [(pointIndex + points.length - 1) % points.length];
12742       float [] previousPreviousPoint = points [(pointIndex + points.length - 2) % points.length];
12743       float sideLength = (float)Point2D.distance(
12744           previousPoint [0], previousPoint [1],
12745           points [pointIndex][0], points [pointIndex][1]);
12746       float previousSideLength = (float)Point2D.distance(
12747           previousPreviousPoint [0], previousPreviousPoint [1],
12748           previousPoint [0], previousPoint [1]);
12749       if (previousPreviousPoint != point
12750           && sideLength != 0 && previousSideLength != 0) {
12751         // Compute the angle between the side finishing at pointIndex
12752         // and the previous side
12753         float xSideVector = (point [0] - previousPoint [0]) / sideLength;
12754         float ySideVector = (point [1] - previousPoint [1]) / sideLength;
12755         float xPreviousSideVector = (previousPoint [0] - previousPreviousPoint [0]) / previousSideLength;
12756         float yPreviousSideVector = (previousPoint [1] - previousPreviousPoint [1]) / previousSideLength;
12757         int sideAngle = (int)Math.round(180 - Math.toDegrees(Math.atan2(
12758             ySideVector * xPreviousSideVector - xSideVector * yPreviousSideVector,
12759             xSideVector * xPreviousSideVector + ySideVector * yPreviousSideVector)));
12760         if (sideAngle > 180) {
12761           sideAngle -= 360;
12762         }
12763         return sideAngle;
12764       }
12765       if (sideLength == 0) {
12766         return 0;
12767       } else {
12768         return (int)Math.round(Math.toDegrees(Math.atan2(
12769             previousPoint [1] - point [1],
12770             point [0] - previousPoint [0])));
12771       }
12772     }
12773 
12774     protected void showRoomAngleFeedback(Room room, int pointIndex) {
12775       float [][] points = room.getPoints();
12776       if (this.roomSideAngleToolTipFeedback != null
12777           && this.roomSideAngleToolTipFeedback.length() > 0
12778           && points.length > 2) {
12779         float [] previousPoint = points [(pointIndex + points.length - 1) % points.length];
12780         float [] previousPreviousPoint = points [(pointIndex + points.length - 2) % points.length];
12781         if (getRoomSideAngle(room, pointIndex) > 0) {
12782           getView().setAngleFeedback(previousPoint [0], previousPoint [1],
12783               previousPreviousPoint [0], previousPreviousPoint [1],
12784               points [pointIndex][0], points [pointIndex][1]);
12785         } else {
12786           getView().setAngleFeedback(previousPoint [0], previousPoint [1],
12787               points [pointIndex][0], points [pointIndex][1],
12788               previousPreviousPoint [0], previousPreviousPoint [1]);
12789         }
12790       }
12791     }
12792   }
12793 
12794   /**
12795    * Room drawing state. This state manages room creation at mouse press.
12796    */
12797   private class RoomDrawingState extends AbstractRoomState {
12798     private float                  xPreviousPoint;
12799     private float                  yPreviousPoint;
12800     private Room                   newRoom;
12801     private float []               newPoint;
12802     private List<Selectable>       oldSelection;
12803     private boolean                oldBasePlanLocked;
12804     private boolean                oldAllLevelsSelection;
12805     private boolean                magnetismEnabled;
12806     private boolean                alignmentActivated;
12807     private long                   lastPointCreationTime;
12808 
12809     @Override
12810     public Mode getMode() {
12811       return Mode.ROOM_CREATION;
12812     }
12813 
12814     @Override
12815     public boolean isModificationState() {
12816       return true;
12817     }
12818 
12819     @Override
12820     public boolean isBasePlanModificationState() {
12821       return true;
12822     }
12823 
12824     @Override
12825     public void setMode(Mode mode) {
12826       // Escape current creation and change state to matching mode
12827       escape();
12828       if (mode == Mode.SELECTION) {
12829         setState(getSelectionState());
12830       } else if (mode == Mode.PANNING) {
12831         setState(getPanningState());
12832       } else if (mode == Mode.WALL_CREATION) {
12833         setState(getWallCreationState());
12834       } else if (mode == Mode.POLYLINE_CREATION) {
12835         setState(getPolylineCreationState());
12836       } else if (mode == Mode.DIMENSION_LINE_CREATION) {
12837         setState(getDimensionLineCreationState());
12838       } else if (mode == Mode.LABEL_CREATION) {
12839         setState(getLabelCreationState());
12840       }
12841     }
12842 
12843     @Override
12844     public void enter() {
12845       super.enter();
12846       this.oldSelection = home.getSelectedItems();
12847       this.oldBasePlanLocked = home.isBasePlanLocked();
12848       this.oldAllLevelsSelection = home.isAllLevelsSelection();
12849       this.newRoom = null;
12850       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
12851       toggleMagnetism(wasMagnetismToggledLastMousePress());
12852       if (this.magnetismEnabled) {
12853         // Find the closest wall or room point to current mouse location
12854         PointMagnetizedToClosestWallOrRoomPoint point = new PointMagnetizedToClosestWallOrRoomPoint(
12855             getXLastMouseMove(), getYLastMouseMove());
12856         if (point.isMagnetized()) {
12857           this.xPreviousPoint = point.getX();
12858           this.yPreviousPoint = point.getY();
12859         } else {
12860           RoomPointWithAngleMagnetism pointWithAngleMagnetism = new RoomPointWithAngleMagnetism(null, -1,
12861               getXLastMouseMove(), getYLastMouseMove(), getXLastMouseMove(), getYLastMouseMove());
12862           this.xPreviousPoint = pointWithAngleMagnetism.getX();
12863           this.yPreviousPoint = pointWithAngleMagnetism.getY();
12864         }
12865         getView().setAlignmentFeedback(Room.class, null,
12866             this.xPreviousPoint, this.yPreviousPoint, point.isMagnetized());
12867       } else {
12868         this.xPreviousPoint = getXLastMousePress();
12869         this.yPreviousPoint = getYLastMousePress();
12870         getView().setAlignmentFeedback(Room.class, null,
12871             this.xPreviousPoint, this.yPreviousPoint, false);
12872       }
12873       deselectAll();
12874     }
12875 
12876     @Override
12877     public void moveMouse(float x, float y) {
12878       PlanView planView = getView();
12879       // Compute the coordinates where current edit room point should be moved
12880       float xEnd = x;
12881       float yEnd = y;
12882       boolean magnetizedPoint = false;
12883       if (this.alignmentActivated) {
12884         PointWithAngleMagnetism pointWithAngleMagnetism = new PointWithAngleMagnetism(
12885             this.xPreviousPoint, this.yPreviousPoint, x, y, preferences.getLengthUnit(), planView.getPixelLength());
12886         xEnd = pointWithAngleMagnetism.getX();
12887         yEnd = pointWithAngleMagnetism.getY();
12888       } else if (this.magnetismEnabled) {
12889         // Find the closest wall or room point to current mouse location
12890         PointMagnetizedToClosestWallOrRoomPoint point = this.newRoom != null
12891             ? new PointMagnetizedToClosestWallOrRoomPoint(this.newRoom, this.newRoom.getPointCount() - 1, x, y)
12892             : new PointMagnetizedToClosestWallOrRoomPoint(x, y);
12893         magnetizedPoint = point.isMagnetized();
12894         if (magnetizedPoint) {
12895           xEnd = point.getX();
12896           yEnd = point.getY();
12897         } else {
12898           // Use magnetism if closest wall point is too far
12899           int editedPointIndex = this.newRoom != null
12900               ? this.newRoom.getPointCount() - 1
12901               : -1;
12902           RoomPointWithAngleMagnetism pointWithAngleMagnetism = new RoomPointWithAngleMagnetism(
12903               this.newRoom, editedPointIndex, this.xPreviousPoint, this.yPreviousPoint, x, y);
12904           xEnd = pointWithAngleMagnetism.getX();
12905           yEnd = pointWithAngleMagnetism.getY();
12906         }
12907       }
12908 
12909       // If current room doesn't exist
12910       if (this.newRoom == null) {
12911         // Create a new one
12912         this.newRoom = createAndSelectRoom(this.xPreviousPoint, this.yPreviousPoint, xEnd, yEnd);
12913       } else if (this.newPoint != null) {
12914         // Add a point to current room
12915         float [][] points = this.newRoom.getPoints();
12916         this.xPreviousPoint = points [points.length - 1][0];
12917         this.yPreviousPoint = points [points.length - 1][1];
12918         this.newRoom.addPoint(xEnd, yEnd);
12919         this.newPoint = null;
12920       } else {
12921         // Otherwise update its last point
12922         this.newRoom.setPoint(xEnd, yEnd, this.newRoom.getPointCount() - 1);
12923       }
12924       planView.setToolTipFeedback(
12925           getToolTipFeedbackText(this.newRoom, this.newRoom.getPointCount() - 1), x, y);
12926       planView.setAlignmentFeedback(Room.class, this.newRoom,
12927           xEnd, yEnd, magnetizedPoint);
12928       showRoomAngleFeedback(this.newRoom, this.newRoom.getPointCount() - 1);
12929       planView.setDimensionLinesFeedback(getTriangulationDimensionLines(this.newRoom, this.newRoom.getPointCount() - 1));
12930 
12931       // Ensure point at (x,y) is visible
12932       planView.makePointVisible(x, y);
12933     }
12934 
12935     /**
12936      * Returns a new room instance with one side between (<code>xStart</code>,
12937      * <code>yStart</code>) and (<code>xEnd</code>, <code>yEnd</code>) points.
12938      * The new room is added to home and selected
12939      */
12940     private Room createAndSelectRoom(float xStart, float yStart,
12941                                      float xEnd, float yEnd) {
12942       Room newRoom = createRoom(new float [][] {{xStart, yStart}, {xEnd, yEnd}});
12943       // Let's consider that points outside of home will create  by default a room with no ceiling
12944       Area insideWallsArea = getInsideWallsArea();
12945       newRoom.setCeilingVisible(insideWallsArea.contains(xStart, yStart));
12946       selectItem(newRoom);
12947       return newRoom;
12948     }
12949 
12950     @Override
12951     public void pressMouse(float x, float y, int clickCount,
12952                            boolean shiftDown, boolean duplicationActivated) {
12953       if (clickCount == 2) {
12954         if (this.newRoom == null) {
12955           // Try to guess the room that contains the point (x,y)
12956           this.newRoom = createRoomAt(x, y);
12957           if (this.newRoom != null) {
12958             selectItem(this.newRoom);
12959           }
12960         }
12961         validateDrawnRoom();
12962       } else {
12963         endRoomSide();
12964       }
12965     }
12966 
12967     private void validateDrawnRoom() {
12968       if (this.newRoom != null) {
12969         float [][] points = this.newRoom.getPoints();
12970         if (points.length < 3) {
12971           // Delete current created room if it doesn't have more than 2 clicked points
12972           home.deleteRoom(this.newRoom);
12973         } else {
12974           // Post room creation to undo support
12975           postCreateRooms(Arrays.asList(new Room [] {this.newRoom}),
12976               this.oldSelection, this.oldBasePlanLocked, this.oldAllLevelsSelection);
12977         }
12978       }
12979       if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
12980         setState(getSelectionState());
12981       } else {
12982         // Change state to RoomCreationState
12983         setState(getRoomCreationState());
12984       }
12985     }
12986 
12987     private void endRoomSide() {
12988       // Create a new room side only when its length is greater than zero
12989       if (this.newRoom != null
12990           && getRoomSideLength(this.newRoom, this.newRoom.getPointCount() - 1) > 0) {
12991         this.newPoint = new float [2];
12992         // Let's consider that any point outside of home will create
12993         // by default a room with no ceiling
12994         if (this.newRoom.isCeilingVisible()) {
12995           float [][] roomPoints = this.newRoom.getPoints();
12996           float [] lastPoint = roomPoints [roomPoints.length - 1];
12997           if (!getInsideWallsArea().contains(lastPoint [0], lastPoint [1])) {
12998             this.newRoom.setCeilingVisible(false);
12999           }
13000         }
13001       }
13002     }
13003 
13004     /**
13005      * Returns the room matching the closed path that contains the point at the given
13006      * coordinates or <code>null</code> if there's no closed path at this point.
13007      */
13008     private Room createRoomAt(float x, float y) {
13009       for (GeneralPath roomPath : getRoomPathsFromWalls()) {
13010         if (roomPath.contains(x, y)) {
13011           // Add to roomPath the doorstep between the room border and the middle of the doors and windows
13012           // with an elevation equal to zero that intersects with roomPath
13013           for (HomePieceOfFurniture piece : getVisibleDoorsAndWindowsAtGround(home.getFurniture())) {
13014             float [][] doorPoints = piece.getPoints();
13015             int intersectionCount = 0;
13016             for (int i = 0; i < doorPoints.length; i++) {
13017               if (roomPath.contains(doorPoints [i][0], doorPoints [i][1])) {
13018                 intersectionCount++;
13019               }
13020             }
13021             if (doorPoints.length == 4) {
13022               float epsilon = 0.05f;
13023               float [][] doorStepPoints = null;
13024               if (piece instanceof HomeDoorOrWindow
13025                   && ((HomeDoorOrWindow)piece).isWallCutOutOnBothSides()) {
13026                 HomeDoorOrWindow door = (HomeDoorOrWindow)piece;
13027                 Level selectedLevel = home.getSelectedLevel();
13028                 Area doorArea = new Area(getPath(doorPoints));
13029                 Area wallsDoorIntersection = new Area();
13030                 for (Wall wall : home.getWalls()) {
13031                   if (wall.isAtLevel(selectedLevel)
13032                       && door.isParallelToWall(wall)) {
13033                     GeneralPath wallPath = getPath(wall.getPoints());
13034                     Area intersectionArea = new Area(wallPath);
13035                     intersectionArea.intersect(doorArea);
13036                     if (!intersectionArea.isEmpty()) {
13037                       HomePieceOfFurniture deeperDoor = door.clone();
13038                       // Increase door depth to ensure the wall will be cut on both sides
13039                       // (doors and windows can't be rotated around horizontal axes)
13040                       deeperDoor.setDepthInPlan(deeperDoor.getDepth() + 4 * wall.getThickness());
13041                       intersectionArea = new Area(wallPath);
13042                       intersectionArea.intersect(new Area(getPath(deeperDoor.getPoints())));
13043                       wallsDoorIntersection.add(intersectionArea);
13044                     }
13045                   }
13046                 }
13047                 if (!wallsDoorIntersection.isEmpty()
13048                     && wallsDoorIntersection.isSingular()) {
13049                   float [][] intersectionPoints = getPathPoints(getPath(wallsDoorIntersection), true);
13050                   if (intersectionPoints.length == 4) {
13051                     // Compute the location of door points at the middle of its wall part
13052                     float doorMiddleY = door.getY()
13053                         + door.getDepth() * (-0.5f + door.getWallDistance() + door.getWallThickness() / 2);
13054                     float halfWidth = door.getWidth() / 2;
13055                     float [] doorMiddlePoints = {door.getX() - halfWidth, doorMiddleY,
13056                                                  door.getX() + halfWidth, doorMiddleY};
13057                     AffineTransform rotation = AffineTransform.getRotateInstance(
13058                         door.getAngle(), door.getX(), door.getY());
13059                     rotation.transform(doorMiddlePoints, 0, doorMiddlePoints, 0, 2);
13060 
13061                     for (int i = 0; i < intersectionPoints.length - 1; i++) {
13062                       // Check point in room with rectangle intersection test otherwise we miss some points
13063                       if (roomPath.intersects(intersectionPoints [i][0] - epsilon / 2,
13064                               intersectionPoints [i][1] - epsilon / 2, epsilon, epsilon)) {
13065                         int inPoint1 = i;
13066                         int outPoint1;
13067                         int outPoint2;
13068                         if (roomPath.intersects(intersectionPoints [i + 1][0] - epsilon / 2,
13069                                  intersectionPoints [i + 1][1] - epsilon / 2, epsilon, epsilon)) {
13070                           outPoint2 = (i + 2) % 4;
13071                           outPoint1 = (i + 3) % 4;
13072                         } else if (roomPath.intersects(intersectionPoints [(i + 3) % 4][0] - epsilon / 2,
13073                             intersectionPoints [(i + 3) % 4][1] - epsilon / 2, epsilon, epsilon)) {
13074                           outPoint1 = (i + 1) % 4;
13075                           outPoint2 = (i + 2) % 4;
13076                         } else {
13077                           // May happen if door intersects room path at only one point when door is larger that room side
13078                           break;
13079                         }
13080                         if (Point2D.distanceSq(intersectionPoints [inPoint1][0], intersectionPoints [inPoint1][1],
13081                                 doorMiddlePoints [0], doorMiddlePoints [1])
13082                             < Point2D.distanceSq(intersectionPoints [inPoint1][0], intersectionPoints [inPoint1][1],
13083                                 doorMiddlePoints [2], doorMiddlePoints [3])) {
13084                           intersectionPoints [outPoint1][0] = doorMiddlePoints [0];
13085                           intersectionPoints [outPoint1][1] = doorMiddlePoints [1];
13086                           intersectionPoints [outPoint2][0] = doorMiddlePoints [2];
13087                           intersectionPoints [outPoint2][1] = doorMiddlePoints [3];
13088                         } else {
13089                           intersectionPoints [outPoint1][0] = doorMiddlePoints [2];
13090                           intersectionPoints [outPoint1][1] = doorMiddlePoints [3];
13091                           intersectionPoints [outPoint2][0] = doorMiddlePoints [0];
13092                           intersectionPoints [outPoint2][1] = doorMiddlePoints [1];
13093                         }
13094 
13095                         doorStepPoints = intersectionPoints;
13096                         break;
13097                       }
13098                     }
13099                   }
13100                 }
13101               }
13102               if (doorStepPoints == null
13103                   && intersectionCount == 2) {
13104                 // Find the intersection of the door with home walls
13105                 Area wallsDoorIntersection = new Area(getWallsArea(false));
13106                 wallsDoorIntersection.intersect(new Area(getPath(doorPoints)));
13107                 // Reduce the size of intersection to its half
13108                 float [][] intersectionPoints = getPathPoints(getPath(wallsDoorIntersection), false);
13109                 if (intersectionPoints.length == 4) {
13110                   for (int i = 0; i < intersectionPoints.length; i++) {
13111                     // Check point in room with rectangle intersection test otherwise we miss some points
13112                     if (roomPath.intersects(intersectionPoints [i][0] - epsilon / 2,
13113                           intersectionPoints [i][1] - epsilon / 2, epsilon, epsilon)) {
13114                       int inPoint1 = i;
13115                       int inPoint2;
13116                       int outPoint1;
13117                       int outPoint2;
13118                       if (roomPath.intersects(intersectionPoints [i + 1][0] - epsilon / 2,
13119                                intersectionPoints [i + 1][1] - epsilon / 2, epsilon, epsilon)) {
13120                         inPoint2 = i + 1;
13121                         outPoint2 = (i + 2) % 4;
13122                         outPoint1 = (i + 3) % 4;
13123                       } else {
13124                         outPoint1 = (i + 1) % 4;
13125                         outPoint2 = (i + 2) % 4;
13126                         inPoint2 = (i + 3) % 4;
13127                       }
13128                       intersectionPoints [outPoint1][0] = (intersectionPoints [outPoint1][0]
13129                           + intersectionPoints [inPoint1][0]) / 2;
13130                       intersectionPoints [outPoint1][1] = (intersectionPoints [outPoint1][1]
13131                           + intersectionPoints [inPoint1][1]) / 2;
13132                       intersectionPoints [outPoint2][0] = (intersectionPoints [outPoint2][0]
13133                           + intersectionPoints [inPoint2][0]) / 2;
13134                       intersectionPoints [outPoint2][1] = (intersectionPoints [outPoint2][1]
13135                           + intersectionPoints [inPoint2][1]) / 2;
13136 
13137                       doorStepPoints = intersectionPoints;
13138                       break;
13139                     }
13140                   }
13141                 }
13142               }
13143 
13144               if (doorStepPoints != null) {
13145                 GeneralPath path = getPath(doorStepPoints);
13146                 // Enlarge the intersection path to ensure its union with room builds only one path
13147                 Rectangle2D bounds2D = path.getBounds2D();
13148                 AffineTransform transform = AffineTransform.getTranslateInstance(bounds2D.getCenterX(), bounds2D.getCenterY());
13149                 double min = Math.min(bounds2D.getWidth(), bounds2D.getHeight());
13150                 double scale = (min + epsilon) / min;
13151                 transform.scale(scale, scale);
13152                 transform.translate(-bounds2D.getCenterX(), -bounds2D.getCenterY());
13153                 Shape doorStepPath = path.createTransformedShape(transform);
13154                 Area halfDoorRoomUnion = new Area(doorStepPath);
13155                 halfDoorRoomUnion.add(new Area(roomPath));
13156                 roomPath = getPath(halfDoorRoomUnion);
13157               }
13158             }
13159           }
13160 
13161           return createRoom(getPathPoints(roomPath, false));
13162         }
13163       }
13164       return null;
13165     }
13166 
13167     /**
13168      * Returns all the visible doors and windows with a null elevation in the given <code>furniture</code>.
13169      */
13170     private List<HomePieceOfFurniture> getVisibleDoorsAndWindowsAtGround(List<HomePieceOfFurniture> furniture) {
13171       List<HomePieceOfFurniture> doorsAndWindows = new ArrayList<HomePieceOfFurniture>(furniture.size());
13172       for (HomePieceOfFurniture piece : furniture) {
13173         if (isPieceOfFurnitureVisibleAtSelectedLevel(piece)
13174             && piece.getElevation() == 0) {
13175           if (piece instanceof HomeFurnitureGroup) {
13176             doorsAndWindows.addAll(getVisibleDoorsAndWindowsAtGround(((HomeFurnitureGroup)piece).getFurniture()));
13177           } else if (piece.isDoorOrWindow()) {
13178             doorsAndWindows.add(piece);
13179           }
13180         }
13181       }
13182       return doorsAndWindows;
13183     }
13184 
13185     @Override
13186     public void setEditionActivated(boolean editionActivated) {
13187       PlanView planView = getView();
13188       if (editionActivated) {
13189         planView.deleteFeedback();
13190         if (this.newRoom == null) {
13191           // Edit previous point
13192           planView.setToolTipEditedProperties(new EditableProperty [] {EditableProperty.X,
13193                                                                        EditableProperty.Y},
13194               new Object [] {this.xPreviousPoint, this.yPreviousPoint},
13195               this.xPreviousPoint, this.yPreviousPoint);
13196         } else {
13197           if (this.newPoint != null) {
13198             // May happen if edition is activated after the user clicked to add a new point
13199             createNextSide();
13200           }
13201           // Edit length and angle
13202           float [][] points = this.newRoom.getPoints();
13203           planView.setToolTipEditedProperties(new EditableProperty [] {EditableProperty.LENGTH,
13204                                                                        EditableProperty.ANGLE},
13205               new Object [] {getRoomSideLength(this.newRoom, points.length - 1),
13206                              getRoomSideAngle(this.newRoom, points.length - 1)},
13207               points [points.length - 1][0], points [points.length - 1][1]);
13208         }
13209       } else {
13210         if (this.newRoom == null) {
13211           // Create a new side once user entered the start point of the room
13212           LengthUnit lengthUnit = preferences.getLengthUnit();
13213           float defaultLength = lengthUnit == LengthUnit.INCH || lengthUnit == LengthUnit.INCH_DECIMALS
13214               ? LengthUnit.footToCentimeter(10) : 300;
13215           this.newRoom = createAndSelectRoom(this.xPreviousPoint, this.yPreviousPoint,
13216                                              this.xPreviousPoint + defaultLength, this.yPreviousPoint);
13217           // Activate automatically second step to let user enter the
13218           // length and angle of the new side
13219           planView.deleteFeedback();
13220           setEditionActivated(true);
13221         } else if (System.currentTimeMillis() - this.lastPointCreationTime < 300) {
13222           // If the user deactivated edition less than 300 ms after activation,
13223           // escape current side creation
13224           escape();
13225         } else {
13226           endRoomSide();
13227           float [][] points = this.newRoom.getPoints();
13228           // If last edited point matches first point validate drawn room
13229           if (points.length > 2
13230               && this.newRoom.getPointIndexAt(points [points.length - 1][0], points [points.length - 1][1], 0.001f) == 0) {
13231             // Remove last currently edited point.
13232             this.newRoom.removePoint(this.newRoom.getPointCount() - 1);
13233             validateDrawnRoom();
13234             return;
13235           }
13236           createNextSide();
13237           // Reactivate automatically second step
13238           planView.deleteToolTipFeedback();
13239           setEditionActivated(true);
13240         }
13241       }
13242     }
13243 
13244     private void createNextSide() {
13245       // Add a point to current room
13246       float [][] points = this.newRoom.getPoints();
13247       this.xPreviousPoint = points [points.length - 1][0];
13248       this.yPreviousPoint = points [points.length - 1][1];
13249       // Create a new side with an angle equal to previous side angle - 90�
13250       double previousSideAngle = Math.PI - Math.atan2(points [points.length - 2][1] - points [points.length - 1][1],
13251           points [points.length - 2][0] - points [points.length - 1][0]);
13252       previousSideAngle -=  Math.PI / 2;
13253       float previousSideLength = getRoomSideLength(this.newRoom, points.length - 1);
13254       this.newRoom.addPoint(
13255           (float)(this.xPreviousPoint + previousSideLength * Math.cos(previousSideAngle)),
13256           (float)(this.yPreviousPoint - previousSideLength * Math.sin(previousSideAngle)));
13257       this.newPoint = null;
13258       this.lastPointCreationTime = System.currentTimeMillis();
13259     }
13260 
13261     @Override
13262     public void updateEditableProperty(EditableProperty editableProperty, Object value) {
13263       PlanView planView = getView();
13264       if (this.newRoom == null) {
13265         float maximumLength = preferences.getLengthUnit().getMaximumLength();
13266         // Update start point of the first wall
13267         switch (editableProperty) {
13268           case X :
13269             this.xPreviousPoint = value != null ? ((Number)value).floatValue() : 0;
13270             this.xPreviousPoint = Math.max(-maximumLength, Math.min(this.xPreviousPoint, maximumLength));
13271             break;
13272           case Y :
13273             this.yPreviousPoint = value != null ? ((Number)value).floatValue() : 0;
13274             this.yPreviousPoint = Math.max(-maximumLength, Math.min(this.yPreviousPoint, maximumLength));
13275             break;
13276         }
13277         planView.setAlignmentFeedback(Room.class, null, this.xPreviousPoint, this.yPreviousPoint, true);
13278         planView.makePointVisible(this.xPreviousPoint, this.yPreviousPoint);
13279       } else {
13280         float [][] roomPoints = this.newRoom.getPoints();
13281         float [] previousPoint = roomPoints [roomPoints.length - 2];
13282         float [] point = roomPoints [roomPoints.length - 1];
13283         float newX;
13284         float newY;
13285         // Update end point of the current room
13286         switch (editableProperty) {
13287           case LENGTH :
13288             float length = value != null ? ((Number)value).floatValue() : 0;
13289             length = Math.max(0.001f, Math.min(length, preferences.getLengthUnit().getMaximumLength()));
13290             double sideAngle = Math.PI - Math.atan2(previousPoint [1] - point [1],
13291                 previousPoint [0] - point [0]);
13292             newX = (float)(previousPoint [0] + length * Math.cos(sideAngle));
13293             newY = (float)(previousPoint [1] - length * Math.sin(sideAngle));
13294             break;
13295           case ANGLE :
13296             sideAngle = Math.toRadians(value != null ? ((Number)value).floatValue() : 0);
13297             if (roomPoints.length > 2) {
13298               sideAngle -= Math.atan2(roomPoints [roomPoints.length - 3][1] - previousPoint [1],
13299                   roomPoints [roomPoints.length - 3][0] - previousPoint [0]);
13300             }
13301             float sideLength = getRoomSideLength(this.newRoom, roomPoints.length - 1);
13302             newX = (float)(previousPoint [0] + sideLength * Math.cos(sideAngle));
13303             newY = (float)(previousPoint [1] - sideLength * Math.sin(sideAngle));
13304             break;
13305           default :
13306             return;
13307         }
13308         this.newRoom.setPoint(newX, newY, roomPoints.length - 1);
13309 
13310         // Update new room
13311         planView.setAlignmentFeedback(Room.class, this.newRoom, newX, newY, false);
13312         showRoomAngleFeedback(this.newRoom, roomPoints.length - 1);
13313         planView.setDimensionLinesFeedback(getTriangulationDimensionLines(this.newRoom, roomPoints.length - 1));
13314         // Ensure room side points are visible
13315         planView.makePointVisible(previousPoint [0], previousPoint [1]);
13316         planView.makePointVisible(newX, newY);
13317       }
13318     }
13319 
13320     @Override
13321     public void toggleMagnetism(boolean magnetismToggled) {
13322       // Compute active magnetism
13323       this.magnetismEnabled = preferences.isMagnetismEnabled()
13324                               ^ magnetismToggled;
13325       // If the new room already exists,
13326       // compute again its last point as if mouse moved
13327       if (this.newRoom != null) {
13328         moveMouse(getXLastMouseMove(), getYLastMouseMove());
13329       }
13330     }
13331 
13332     @Override
13333     public void setAlignmentActivated(boolean alignmentActivated) {
13334       this.alignmentActivated = alignmentActivated;
13335       moveMouse(getXLastMouseMove(), getYLastMouseMove());
13336     }
13337 
13338     @Override
13339     public void escape() {
13340       if (this.newRoom != null
13341           && this.newPoint == null) {
13342         // Remove last currently edited point.
13343         this.newRoom.removePoint(this.newRoom.getPointCount() - 1);
13344       }
13345       validateDrawnRoom();
13346     }
13347 
13348     @Override
13349     public void exit() {
13350       getView().deleteFeedback();
13351       this.newRoom = null;
13352       this.newPoint = null;
13353       this.oldSelection = null;
13354     }
13355   }
13356 
13357   /**
13358    * Room resize state. This state manages room resizing.
13359    */
13360   private class RoomResizeState extends AbstractRoomState {
13361     private Room             selectedRoom;
13362     private int              roomPointIndex;
13363     private float            oldX;
13364     private float            oldY;
13365     private float            deltaXToResizePoint;
13366     private float            deltaYToResizePoint;
13367     private boolean          magnetismEnabled;
13368     private boolean          alignmentActivated;
13369 
13370     @Override
13371     public Mode getMode() {
13372       return Mode.SELECTION;
13373     }
13374 
13375     @Override
13376     public boolean isModificationState() {
13377       return true;
13378     }
13379 
13380     @Override
13381     public boolean isBasePlanModificationState() {
13382       return true;
13383     }
13384 
13385     @Override
13386     public void enter() {
13387       super.enter();
13388       this.selectedRoom = (Room)home.getSelectedItems().get(0);
13389       float margin = getIndicatorMargin();
13390       this.roomPointIndex = this.selectedRoom.getPointIndexAt(
13391           getXLastMousePress(), getYLastMousePress(), margin);
13392       float [][] roomPoints = this.selectedRoom.getPoints();
13393       this.oldX = roomPoints [this.roomPointIndex][0];
13394       this.oldY = roomPoints [this.roomPointIndex][1];
13395       this.deltaXToResizePoint = getXLastMousePress() - this.oldX;
13396       this.deltaYToResizePoint = getYLastMousePress() - this.oldY;
13397       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
13398       toggleMagnetism(wasMagnetismToggledLastMousePress());
13399       PlanView planView = getView();
13400       planView.setResizeIndicatorVisible(true);
13401       planView.setToolTipFeedback(getToolTipFeedbackText(this.selectedRoom, this.roomPointIndex),
13402           getXLastMousePress(), getYLastMousePress());
13403       showRoomAngleFeedback(this.selectedRoom, this.roomPointIndex);
13404       planView.setDimensionLinesFeedback(getTriangulationDimensionLines(this.selectedRoom, this.roomPointIndex));
13405     }
13406 
13407     @Override
13408     public void moveMouse(float x, float y) {
13409       PlanView planView = getView();
13410       float newX = x - this.deltaXToResizePoint;
13411       float newY = y - this.deltaYToResizePoint;
13412       float [][] roomPoints = this.selectedRoom.getPoints();
13413       int previousPointIndex = this.roomPointIndex == 0
13414           ? roomPoints.length - 1
13415           : this.roomPointIndex - 1;
13416       float xPreviousPoint = roomPoints [previousPointIndex][0];
13417       float yPreviousPoint = roomPoints [previousPointIndex][1];
13418       boolean magnetizedPoint = false;
13419       if (this.alignmentActivated) {
13420         PointWithAngleMagnetism pointWithAngleMagnetism = new PointWithAngleMagnetism(
13421             xPreviousPoint, yPreviousPoint, newX, newY, preferences.getLengthUnit(), planView.getPixelLength());
13422         newX = pointWithAngleMagnetism.getX();
13423         newY = pointWithAngleMagnetism.getY();
13424       } else if (this.magnetismEnabled) {
13425         // Find the closest wall or room point to current mouse location
13426         PointMagnetizedToClosestWallOrRoomPoint point = new PointMagnetizedToClosestWallOrRoomPoint(
13427             this.selectedRoom, this.roomPointIndex, newX, newY);
13428         magnetizedPoint = point.isMagnetized();
13429         if (magnetizedPoint) {
13430           newX = point.getX();
13431           newY = point.getY();
13432         } else {
13433           // Use magnetism if closest wall point is too far
13434           RoomPointWithAngleMagnetism pointWithAngleMagnetism = new RoomPointWithAngleMagnetism(
13435               this.selectedRoom, this.roomPointIndex, xPreviousPoint, yPreviousPoint, newX, newY);
13436           newX = pointWithAngleMagnetism.getX();
13437           newY = pointWithAngleMagnetism.getY();
13438         }
13439       }
13440       moveRoomPoint(this.selectedRoom, newX, newY, this.roomPointIndex);
13441 
13442       planView.setToolTipFeedback(getToolTipFeedbackText(this.selectedRoom, this.roomPointIndex), x, y);
13443       planView.setAlignmentFeedback(Room.class, this.selectedRoom, newX, newY, magnetizedPoint);
13444       showRoomAngleFeedback(this.selectedRoom, this.roomPointIndex);
13445       planView.setDimensionLinesFeedback(getTriangulationDimensionLines(this.selectedRoom, this.roomPointIndex));
13446       // Ensure point at (x,y) is visible
13447       planView.makePointVisible(x, y);
13448     }
13449 
13450     @Override
13451     public void releaseMouse(float x, float y) {
13452       postRoomResize(this.selectedRoom, this.oldX, this.oldY, this.roomPointIndex);
13453       setState(getSelectionState());
13454     }
13455 
13456     @Override
13457     public void toggleMagnetism(boolean magnetismToggled) {
13458       // Compute active magnetism
13459       this.magnetismEnabled = preferences.isMagnetismEnabled()
13460                               ^ magnetismToggled;
13461       moveMouse(getXLastMouseMove(), getYLastMouseMove());
13462     }
13463 
13464     @Override
13465     public void setAlignmentActivated(boolean alignmentActivated) {
13466       this.alignmentActivated = alignmentActivated;
13467       moveMouse(getXLastMouseMove(), getYLastMouseMove());
13468     }
13469 
13470     @Override
13471     public void escape() {
13472       moveRoomPoint(this.selectedRoom, this.oldX, this.oldY, this.roomPointIndex);
13473       setState(getSelectionState());
13474     }
13475 
13476     @Override
13477     public void exit() {
13478       PlanView planView = getView();
13479       planView.setResizeIndicatorVisible(false);
13480       planView.deleteFeedback();
13481       this.selectedRoom = null;
13482     }
13483   }
13484 
13485   /**
13486    * Room name offset state. This state manages room name offset.
13487    */
13488   private class RoomNameOffsetState extends ControllerState {
13489     private Room    selectedRoom;
13490     private float   oldNameXOffset;
13491     private float   oldNameYOffset;
13492     private float   xLastMouseMove;
13493     private float   yLastMouseMove;
13494     private boolean alignmentActivated;
13495 
13496     @Override
13497     public Mode getMode() {
13498       return Mode.SELECTION;
13499     }
13500 
13501     @Override
13502     public boolean isModificationState() {
13503       return true;
13504     }
13505 
13506     @Override
13507     public void enter() {
13508       this.selectedRoom = (Room)home.getSelectedItems().get(0);
13509       this.oldNameXOffset = this.selectedRoom.getNameXOffset();
13510       this.oldNameYOffset = this.selectedRoom.getNameYOffset();
13511       this.xLastMouseMove = getXLastMousePress();
13512       this.yLastMouseMove = getYLastMousePress();
13513       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
13514       PlanView planView = getView();
13515       planView.setResizeIndicatorVisible(true);
13516     }
13517 
13518     @Override
13519     public void moveMouse(float x, float y) {
13520       if (this.alignmentActivated) {
13521         PointWithAngleMagnetism alignedPoint = new PointWithAngleMagnetism(getXLastMousePress(), getYLastMousePress(),
13522             x, y, preferences.getLengthUnit(), getView().getPixelLength(), 4);
13523         x = alignedPoint.getX();
13524         y = alignedPoint.getY();
13525       }
13526       this.selectedRoom.setNameXOffset(this.selectedRoom.getNameXOffset() + x - this.xLastMouseMove);
13527       this.selectedRoom.setNameYOffset(this.selectedRoom.getNameYOffset() + y - this.yLastMouseMove);
13528       this.xLastMouseMove = x;
13529       this.yLastMouseMove = y;
13530 
13531       // Ensure point at (x,y) is visible
13532       getView().makePointVisible(x, y);
13533     }
13534 
13535     @Override
13536     public void releaseMouse(float x, float y) {
13537       postRoomNameOffset(this.selectedRoom, this.oldNameXOffset, this.oldNameYOffset);
13538       setState(getSelectionState());
13539     }
13540 
13541     @Override
13542     public void setAlignmentActivated(boolean alignmentActivated) {
13543       this.alignmentActivated = alignmentActivated;
13544       moveMouse(getXLastMouseMove(), getYLastMouseMove());
13545     }
13546 
13547     @Override
13548     public void escape() {
13549       this.selectedRoom.setNameXOffset(this.oldNameXOffset);
13550       this.selectedRoom.setNameYOffset(this.oldNameYOffset);
13551       setState(getSelectionState());
13552     }
13553 
13554     @Override
13555     public void exit() {
13556       getView().setResizeIndicatorVisible(false);
13557       this.selectedRoom = null;
13558     }
13559   }
13560 
13561   /**
13562    * Room name rotation state. This state manages the name rotation of a room.
13563    */
13564   private class RoomNameRotationState extends ControllerState {
13565     private static final int STEP_COUNT = 24;
13566 
13567     private Room     selectedRoom;
13568     private float    oldNameAngle;
13569     private float    angleMousePress;
13570     private boolean  magnetismEnabled;
13571     private boolean  alignmentActivated;
13572 
13573     @Override
13574     public Mode getMode() {
13575       return Mode.SELECTION;
13576     }
13577 
13578     @Override
13579     public boolean isModificationState() {
13580       return true;
13581     }
13582 
13583     @Override
13584     public void enter() {
13585       this.selectedRoom = (Room)home.getSelectedItems().get(0);
13586       this.angleMousePress = (float)Math.atan2(this.selectedRoom.getYCenter() + this.selectedRoom.getNameYOffset() - getYLastMousePress(),
13587           getXLastMousePress() - this.selectedRoom.getXCenter() - this.selectedRoom.getNameXOffset());
13588       this.oldNameAngle = this.selectedRoom.getNameAngle();
13589       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
13590       this.magnetismEnabled = preferences.isMagnetismEnabled()
13591           ^ wasMagnetismToggledLastMousePress();
13592       PlanView planView = getView();
13593       planView.setResizeIndicatorVisible(true);
13594     }
13595 
13596     @Override
13597     public void moveMouse(float x, float y) {
13598       if (x != this.selectedRoom.getXCenter() + this.selectedRoom.getNameXOffset()
13599           || y != this.selectedRoom.getYCenter() + this.selectedRoom.getNameYOffset()) {
13600         // Compute the new angle of the room name
13601         float angleMouseMove = (float)Math.atan2(this.selectedRoom.getYCenter() + this.selectedRoom.getNameYOffset() - y,
13602             x - this.selectedRoom.getXCenter() - this.selectedRoom.getNameXOffset());
13603         float newAngle = this.oldNameAngle - angleMouseMove + this.angleMousePress;
13604 
13605         if (this.alignmentActivated
13606             || this.magnetismEnabled) {
13607           float angleStep = 2 * (float)Math.PI / STEP_COUNT;
13608           // Compute angles closest to a step angle (multiple of angleStep)
13609           newAngle = Math.round(newAngle / angleStep) * angleStep;
13610         }
13611 
13612         // Update room name new angle
13613         this.selectedRoom.setNameAngle(newAngle);
13614         getView().makePointVisible(x, y);
13615       }
13616     }
13617 
13618     @Override
13619     public void releaseMouse(float x, float y) {
13620       postRoomNameRotation(this.selectedRoom, this.oldNameAngle);
13621       setState(getSelectionState());
13622     }
13623 
13624     @Override
13625     public void toggleMagnetism(boolean magnetismToggled) {
13626       // Compute active magnetism
13627       this.magnetismEnabled = preferences.isMagnetismEnabled()
13628                               ^ magnetismToggled;
13629       // Compute again angle as if mouse moved
13630       moveMouse(getXLastMouseMove(), getYLastMouseMove());
13631     }
13632 
13633     @Override
13634     public void setAlignmentActivated(boolean alignmentActivated) {
13635       this.alignmentActivated = alignmentActivated;
13636       moveMouse(getXLastMouseMove(), getYLastMouseMove());
13637     }
13638 
13639     @Override
13640     public void escape() {
13641       this.selectedRoom.setNameAngle(this.oldNameAngle);
13642       setState(getSelectionState());
13643     }
13644 
13645     @Override
13646     public void exit() {
13647       getView().setResizeIndicatorVisible(false);
13648       this.selectedRoom = null;
13649     }
13650   }
13651 
13652   /**
13653    * Room area offset state. This state manages room area offset.
13654    */
13655   private class RoomAreaOffsetState extends ControllerState {
13656     private Room    selectedRoom;
13657     private float   oldAreaXOffset;
13658     private float   oldAreaYOffset;
13659     private float   xLastMouseMove;
13660     private float   yLastMouseMove;
13661     private boolean alignmentActivated;
13662 
13663     @Override
13664     public Mode getMode() {
13665       return Mode.SELECTION;
13666     }
13667 
13668     @Override
13669     public boolean isModificationState() {
13670       return true;
13671     }
13672 
13673     @Override
13674     public void enter() {
13675       this.selectedRoom = (Room)home.getSelectedItems().get(0);
13676       this.oldAreaXOffset = this.selectedRoom.getAreaXOffset();
13677       this.oldAreaYOffset = this.selectedRoom.getAreaYOffset();
13678       this.xLastMouseMove = getXLastMousePress();
13679       this.yLastMouseMove = getYLastMousePress();
13680       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
13681       PlanView planView = getView();
13682       planView.setResizeIndicatorVisible(true);
13683     }
13684 
13685     @Override
13686     public void moveMouse(float x, float y) {
13687       if (this.alignmentActivated) {
13688         PointWithAngleMagnetism alignedPoint = new PointWithAngleMagnetism(getXLastMousePress(), getYLastMousePress(),
13689             x, y, preferences.getLengthUnit(), getView().getPixelLength(), 4);
13690         x = alignedPoint.getX();
13691         y = alignedPoint.getY();
13692       }
13693       this.selectedRoom.setAreaXOffset(this.selectedRoom.getAreaXOffset() + x - this.xLastMouseMove);
13694       this.selectedRoom.setAreaYOffset(this.selectedRoom.getAreaYOffset() + y - this.yLastMouseMove);
13695       this.xLastMouseMove = x;
13696       this.yLastMouseMove = y;
13697 
13698       // Ensure point at (x,y) is visible
13699       getView().makePointVisible(x, y);
13700     }
13701 
13702     @Override
13703     public void releaseMouse(float x, float y) {
13704       postRoomAreaOffset(this.selectedRoom, this.oldAreaXOffset, this.oldAreaYOffset);
13705       setState(getSelectionState());
13706     }
13707 
13708     @Override
13709     public void setAlignmentActivated(boolean alignmentActivated) {
13710       this.alignmentActivated = alignmentActivated;
13711       moveMouse(getXLastMouseMove(), getYLastMouseMove());
13712     }
13713 
13714     @Override
13715     public void escape() {
13716       this.selectedRoom.setAreaXOffset(this.oldAreaXOffset);
13717       this.selectedRoom.setAreaYOffset(this.oldAreaYOffset);
13718       setState(getSelectionState());
13719     }
13720 
13721     @Override
13722     public void exit() {
13723       getView().setResizeIndicatorVisible(false);
13724       this.selectedRoom = null;
13725     }
13726   }
13727 
13728   /**
13729    * Room area rotation state. This state manages the area rotation of a room.
13730    */
13731   private class RoomAreaRotationState extends ControllerState {
13732     private static final int  STEP_COUNT = 24;
13733 
13734     private Room     selectedRoom;
13735     private float    oldAreaAngle;
13736     private float    angleMousePress;
13737     private boolean  magnetismEnabled;
13738     private boolean  alignmentActivated;
13739 
13740     @Override
13741     public Mode getMode() {
13742       return Mode.SELECTION;
13743     }
13744 
13745     @Override
13746     public boolean isModificationState() {
13747       return true;
13748     }
13749 
13750     @Override
13751     public void enter() {
13752       this.selectedRoom = (Room)home.getSelectedItems().get(0);
13753       this.angleMousePress = (float)Math.atan2(this.selectedRoom.getYCenter() + this.selectedRoom.getAreaYOffset() - getYLastMousePress(),
13754           getXLastMousePress() - this.selectedRoom.getXCenter() - this.selectedRoom.getAreaXOffset());
13755       this.oldAreaAngle = this.selectedRoom.getAreaAngle();
13756       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
13757       this.magnetismEnabled = preferences.isMagnetismEnabled()
13758           ^ wasMagnetismToggledLastMousePress();
13759       PlanView planView = getView();
13760       planView.setResizeIndicatorVisible(true);
13761     }
13762 
13763     @Override
13764     public void moveMouse(float x, float y) {
13765       if (x != this.selectedRoom.getXCenter() + this.selectedRoom.getAreaXOffset()
13766           || y != this.selectedRoom.getYCenter() + this.selectedRoom.getAreaYOffset()) {
13767         // Compute the new angle of the room area
13768         float angleMouseMove = (float)Math.atan2(this.selectedRoom.getYCenter() + this.selectedRoom.getAreaYOffset() - y,
13769             x - this.selectedRoom.getXCenter() - this.selectedRoom.getAreaXOffset());
13770         float newAngle = this.oldAreaAngle - angleMouseMove + this.angleMousePress;
13771 
13772         if (this.alignmentActivated
13773             || this.magnetismEnabled) {
13774           float angleStep = 2 * (float)Math.PI / STEP_COUNT;
13775           // Compute angles closest to a step angle (multiple of angleStep)
13776           newAngle = Math.round(newAngle / angleStep) * angleStep;
13777         }
13778 
13779         // Update room area new angle
13780         this.selectedRoom.setAreaAngle(newAngle);
13781         getView().makePointVisible(x, y);
13782       }
13783     }
13784 
13785     @Override
13786     public void releaseMouse(float x, float y) {
13787       postRoomAreaRotation(this.selectedRoom, this.oldAreaAngle);
13788       setState(getSelectionState());
13789     }
13790 
13791     @Override
13792     public void toggleMagnetism(boolean magnetismToggled) {
13793       // Compute active magnetism
13794       this.magnetismEnabled = preferences.isMagnetismEnabled()
13795                               ^ magnetismToggled;
13796       // Compute again angle as if mouse moved
13797       moveMouse(getXLastMouseMove(), getYLastMouseMove());
13798     }
13799 
13800     @Override
13801     public void setAlignmentActivated(boolean alignmentActivated) {
13802       this.alignmentActivated = alignmentActivated;
13803       moveMouse(getXLastMouseMove(), getYLastMouseMove());
13804     }
13805 
13806     @Override
13807     public void escape() {
13808       this.selectedRoom.setAreaAngle(this.oldAreaAngle);
13809       setState(getSelectionState());
13810     }
13811 
13812     @Override
13813     public void exit() {
13814       getView().setResizeIndicatorVisible(false);
13815       this.selectedRoom = null;
13816     }
13817   }
13818 
13819   /**
13820    * Polyline creation state. This state manages transition to
13821    * other modes, and initial polyline creation.
13822    */
13823   private class PolylineCreationState extends AbstractModeChangeState {
13824     @Override
13825     public Mode getMode() {
13826       return Mode.POLYLINE_CREATION;
13827     }
13828 
13829     @Override
13830     public void enter() {
13831       getView().setCursor(PlanView.CursorType.DRAW);
13832     }
13833 
13834     @Override
13835     public void pressMouse(float x, float y, int clickCount,
13836                            boolean shiftDown, boolean duplicationActivated) {
13837       // Change state to PolylineDrawingState
13838       setState(getPolylineDrawingState());
13839     }
13840 
13841     @Override
13842     public void setEditionActivated(boolean editionActivated) {
13843       if (editionActivated) {
13844         setState(getPolylineDrawingState());
13845         PlanController.this.setEditionActivated(editionActivated);
13846       }
13847     }
13848   }
13849 
13850   /**
13851    * Polyline modification state.
13852    */
13853   private abstract class AbstractPolylineState extends ControllerState {
13854     private String polylineSegmentLengthToolTipFeedback;
13855     private String polylineSegmentAngleToolTipFeedback;
13856 
13857     @Override
13858     public void enter() {
13859       this.polylineSegmentLengthToolTipFeedback = preferences.getLocalizedString(
13860           PlanController.class, "polylineSegmentLengthToolTipFeedback");
13861       try {
13862         this.polylineSegmentAngleToolTipFeedback = preferences.getLocalizedString(
13863             PlanController.class, "polylineSegmentAngleToolTipFeedback");
13864       } catch (IllegalArgumentException ex) {
13865         // This tool tip part is optional
13866       }
13867     }
13868 
13869     protected String getToolTipFeedbackText(Polyline polyline, int pointIndex) {
13870       float length = getPolylineSegmentLength(polyline, pointIndex);
13871       int angle = getPolylineSegmentAngle(polyline, pointIndex);
13872       String toolTipFeedbackText = "<html>" + String.format(this.polylineSegmentLengthToolTipFeedback, preferences.getLengthUnit().getFormatWithUnit().format(length));
13873       if (this.polylineSegmentAngleToolTipFeedback != null
13874           && this.polylineSegmentAngleToolTipFeedback.length() > 0) {
13875         toolTipFeedbackText += "<br>" + String.format(this.polylineSegmentAngleToolTipFeedback, angle);
13876       }
13877       return toolTipFeedbackText;
13878     }
13879 
13880     protected float getPolylineSegmentLength(Polyline polyline, int pointIndex) {
13881       if (pointIndex == 0 && !polyline.isClosedPath()) {
13882         // Return the length of the first segment for index 0 of a not closed path
13883         pointIndex++;
13884       }
13885       float [][] points = polyline.getPoints();
13886       float [] previousPoint = points [(pointIndex + points.length - 1) % points.length];
13887       return (float)Point2D.distance(previousPoint [0], previousPoint [1],
13888           points [pointIndex][0], points [pointIndex][1]);
13889     }
13890 
13891     /**
13892      * Returns polyline segment angle at the given point index in degrees.
13893      */
13894     protected Integer getPolylineSegmentAngle(Polyline polyline, int pointIndex) {
13895       if (pointIndex == 0 && !polyline.isClosedPath()) {
13896         // Return the angle of the first segment for index 0 of a not closed path
13897         pointIndex++;
13898       }
13899       float [][] points = polyline.getPoints();
13900       float [] point = points [pointIndex];
13901       float [] previousPoint = points [(pointIndex + points.length - 1) % points.length];
13902       float [] previousPreviousPoint = points [(pointIndex + points.length - 2) % points.length];
13903       float segmentLength = (float)Point2D.distance(
13904           previousPoint [0], previousPoint [1],
13905           points [pointIndex][0], points [pointIndex][1]);
13906       float previousSegmentLength = (float)Point2D.distance(
13907           previousPreviousPoint [0], previousPreviousPoint [1],
13908           previousPoint [0], previousPoint [1]);
13909       if (previousPreviousPoint != point
13910           && segmentLength != 0 && previousSegmentLength != 0) {
13911         // Compute the angle between the segment finishing at pointIndex
13912         // and the previous segment
13913         float xSegmentVector = (point [0] - previousPoint [0]) / segmentLength;
13914         float ySegmentVector = (point [1] - previousPoint [1]) / segmentLength;
13915         float xPreviousSegmentVector = (previousPoint [0] - previousPreviousPoint [0]) / previousSegmentLength;
13916         float yPreviousSegmentVector = (previousPoint [1] - previousPreviousPoint [1]) / previousSegmentLength;
13917         int segmentAngle = (int)Math.round(180 - Math.toDegrees(Math.atan2(
13918             ySegmentVector * xPreviousSegmentVector - xSegmentVector * yPreviousSegmentVector,
13919             xSegmentVector * xPreviousSegmentVector + ySegmentVector * yPreviousSegmentVector)));
13920         if (segmentAngle > 180) {
13921           segmentAngle -= 360;
13922         }
13923         return segmentAngle;
13924       }
13925       if (segmentLength == 0) {
13926         return 0;
13927       } else {
13928         return (int)Math.round(Math.toDegrees(Math.atan2(
13929             previousPoint [1] - point [1],
13930             point [0] - previousPoint [0])));
13931       }
13932     }
13933 
13934     protected void showPolylineAngleFeedback(Polyline polyline, int pointIndex) {
13935       float [][] points = polyline.getPoints();
13936       if (this.polylineSegmentAngleToolTipFeedback != null
13937           && this.polylineSegmentAngleToolTipFeedback.length() > 0
13938           && (pointIndex >= 2
13939              || points.length > 2 && polyline.isClosedPath())) {
13940         float [] previousPoint = points [(pointIndex + points.length - 1) % points.length];
13941         float [] previousPreviousPoint = points [(pointIndex + points.length - 2) % points.length];
13942         getView().setAngleFeedback(previousPoint [0], previousPoint [1],
13943             previousPreviousPoint [0], previousPreviousPoint [1],
13944             points [pointIndex][0], points [pointIndex][1]);
13945       }
13946     }
13947   }
13948 
13949   /**
13950    * Polyline drawing state. This state manages polyline creation at mouse press.
13951    */
13952   private class PolylineDrawingState extends AbstractPolylineState {
13953     private float            xPreviousPoint;
13954     private float            yPreviousPoint;
13955     private Polyline         newPolyline;
13956     private float []         newPoint;
13957     private List<Selectable> oldSelection;
13958     private boolean          oldBasePlanLocked;
13959     private boolean          oldAllLevelsSelection;
13960     private boolean          magnetismEnabled;
13961     private boolean          alignmentActivated;
13962     private boolean          curvedPolyline;
13963     private long             lastPointCreationTime;
13964 
13965     @Override
13966     public Mode getMode() {
13967       return Mode.POLYLINE_CREATION;
13968     }
13969 
13970     @Override
13971     public boolean isModificationState() {
13972       return true;
13973     }
13974 
13975     @Override
13976     public boolean isBasePlanModificationState() {
13977       return true;
13978     }
13979 
13980     @Override
13981     public void setMode(Mode mode) {
13982       // Escape current creation and change state to matching mode
13983       escape();
13984       if (mode == Mode.SELECTION) {
13985         setState(getSelectionState());
13986       } else if (mode == Mode.PANNING) {
13987         setState(getPanningState());
13988       } else if (mode == Mode.WALL_CREATION) {
13989         setState(getWallCreationState());
13990       } else if (mode == Mode.ROOM_CREATION) {
13991         setState(getRoomCreationState());
13992       } else if (mode == Mode.DIMENSION_LINE_CREATION) {
13993         setState(getDimensionLineCreationState());
13994       } else if (mode == Mode.LABEL_CREATION) {
13995         setState(getLabelCreationState());
13996       }
13997     }
13998 
13999     @Override
14000     public void enter() {
14001       super.enter();
14002       this.oldSelection = home.getSelectedItems();
14003       this.oldBasePlanLocked = home.isBasePlanLocked();
14004       this.oldAllLevelsSelection = home.isAllLevelsSelection();
14005       this.newPolyline = null;
14006       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
14007       toggleMagnetism(wasMagnetismToggledLastMousePress());
14008       this.xPreviousPoint = getXLastMousePress();
14009       this.yPreviousPoint = getYLastMousePress();
14010       setDuplicationActivated(wasDuplicationActivatedLastMousePress());
14011       deselectAll();
14012     }
14013 
14014     @Override
14015     public void moveMouse(float x, float y) {
14016       PlanView planView = getView();
14017       // Compute the coordinates where current edit polyline point should be moved
14018       float xEnd = x;
14019       float yEnd = y;
14020       if (this.alignmentActivated
14021           || this.magnetismEnabled) {
14022         PointWithAngleMagnetism pointWithAngleMagnetism = new PointWithAngleMagnetism(
14023             this.xPreviousPoint, this.yPreviousPoint, x, y, preferences.getLengthUnit(), planView.getPixelLength());
14024         xEnd = pointWithAngleMagnetism.getX();
14025         yEnd = pointWithAngleMagnetism.getY();
14026       }
14027 
14028       // If current polyline doesn't exist
14029       if (this.newPolyline == null) {
14030         // Create a new one
14031         this.newPolyline = createAndSelectPolyline(this.xPreviousPoint, this.yPreviousPoint, xEnd, yEnd);
14032       } else if (this.newPoint != null) {
14033         // Add a point to current polyline
14034         float [][] points = this.newPolyline.getPoints();
14035         this.xPreviousPoint = points [points.length - 1][0];
14036         this.yPreviousPoint = points [points.length - 1][1];
14037         this.newPolyline.addPoint(xEnd, yEnd);
14038         this.newPoint [0] = xEnd;
14039         this.newPoint [1] = yEnd;
14040         this.newPoint = null;
14041       } else {
14042         // Otherwise update its last point
14043         this.newPolyline.setPoint(xEnd, yEnd, this.newPolyline.getPointCount() - 1);
14044       }
14045       planView.setAlignmentFeedback(Polyline.class, null, x, y, false);
14046       planView.setToolTipFeedback(
14047           getToolTipFeedbackText(this.newPolyline, this.newPolyline.getPointCount() - 1), x, y);
14048       if (this.newPolyline.getJoinStyle() != Polyline.JoinStyle.CURVED) {
14049         showPolylineAngleFeedback(this.newPolyline, this.newPolyline.getPointCount() - 1);
14050       }
14051 
14052       // Ensure point at (x,y) is visible
14053       planView.makePointVisible(x, y);
14054     }
14055 
14056     /**
14057      * Returns a new polyline instance with one segment between (<code>xStart</code>,
14058      * <code>yStart</code>) and (<code>xEnd</code>, <code>yEnd</code>) points.
14059      * The new polyline is added to home and selected
14060      */
14061     private Polyline createAndSelectPolyline(float xStart, float yStart,
14062                                      float xEnd, float yEnd) {
14063       Polyline newPolyline = createPolyline(new float [][] {{xStart, yStart}, {xEnd, yEnd}});
14064       if (this.curvedPolyline) {
14065         newPolyline.setJoinStyle(Polyline.JoinStyle.CURVED);
14066       }
14067       selectItems(Arrays.asList(new Selectable [] {newPolyline}));
14068       return newPolyline;
14069     }
14070 
14071     @Override
14072     public void pressMouse(float x, float y, int clickCount,
14073                            boolean shiftDown, boolean duplicationActivated) {
14074       if (clickCount == 2) {
14075         if (this.newPolyline != null) {
14076           int pointIndex = this.newPolyline.getPointIndexAt(x, y, getSelectionMargin());
14077           if (pointIndex == 0) {
14078             this.newPolyline.removePoint(this.newPolyline.getPointCount() - 1);
14079             this.newPolyline.setClosedPath(true);
14080           }
14081           validateDrawnPolyline();
14082         } else {
14083           if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
14084             setState(getSelectionState());
14085           } else {
14086             setState(getPolylineCreationState());
14087           }
14088         }
14089       } else {
14090         endPolylineSegment();
14091       }
14092     }
14093 
14094     private void validateDrawnPolyline() {
14095       if (this.newPolyline != null) {
14096         float [][] points = this.newPolyline.getPoints();
14097         if (points.length < 2) {
14098           // Delete current created polyline if it doesn't have more than 2 clicked points
14099           home.deletePolyline(this.newPolyline);
14100         } else {
14101           // Post polyline creation to undo support
14102           postCreatePolylines(Arrays.asList(new Polyline [] {this.newPolyline}),
14103               this.oldSelection, this.oldBasePlanLocked, this.oldAllLevelsSelection);
14104         }
14105       }
14106       if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
14107         setState(getSelectionState());
14108       } else {
14109         // Change state to PolylineCreationState
14110         setState(getPolylineCreationState());
14111       }
14112     }
14113 
14114     private void endPolylineSegment() {
14115       // Create a new polyline segment only when its length is greater than zero
14116       if (this.newPolyline != null
14117           && getPolylineSegmentLength(this.newPolyline, this.newPolyline.getPointCount() - 1) > 0) {
14118         this.newPoint = new float [2];
14119         if (this.newPolyline.getPointCount() <= 2
14120             && this.curvedPolyline
14121             && newPolyline.getJoinStyle() != Polyline.JoinStyle.CURVED) {
14122           // Give a second chance to create a curved polyline
14123           newPolyline.setJoinStyle(Polyline.JoinStyle.CURVED);
14124         }
14125       }
14126     }
14127 
14128     @Override
14129     public void setEditionActivated(boolean editionActivated) {
14130       PlanView planView = getView();
14131       if (editionActivated) {
14132         planView.deleteFeedback();
14133         if (this.newPolyline == null) {
14134           // Edit xStart and yStart
14135           planView.setToolTipEditedProperties(new EditableProperty [] {EditableProperty.X,
14136                                                                        EditableProperty.Y},
14137               new Object [] {this.xPreviousPoint, this.yPreviousPoint},
14138               this.xPreviousPoint, this.yPreviousPoint);
14139         } else {
14140           if (this.newPoint != null) {
14141             // May happen if edition is activated after the user clicked to add a new point
14142             createNextSegment();
14143           }
14144           // Edit length and angle
14145           float [][] points = this.newPolyline.getPoints();
14146           planView.setToolTipEditedProperties(new EditableProperty [] {EditableProperty.LENGTH,
14147                                                                        EditableProperty.ANGLE},
14148               new Object [] {getPolylineSegmentLength(this.newPolyline, points.length - 1),
14149                              getPolylineSegmentAngle(this.newPolyline, points.length - 1)},
14150               points [points.length - 1][0], points [points.length - 1][1]);
14151         }
14152       } else {
14153         if (this.newPolyline == null) {
14154           // Create a new segment once user entered the start point of the polyline
14155           LengthUnit lengthUnit = preferences.getLengthUnit();
14156           float defaultLength = lengthUnit == LengthUnit.INCH || lengthUnit == LengthUnit.INCH_DECIMALS
14157               ? LengthUnit.footToCentimeter(10) : 300;
14158           this.newPolyline = createAndSelectPolyline(this.xPreviousPoint, this.yPreviousPoint,
14159               this.xPreviousPoint + defaultLength, this.yPreviousPoint);
14160           // Activate automatically second step to let user enter the
14161           // length and angle of the new segment
14162           planView.deleteFeedback();
14163           setEditionActivated(true);
14164         } else if (System.currentTimeMillis() - this.lastPointCreationTime < 300) {
14165           // If the user deactivated edition less than 300 ms after activation,
14166           // escape current segment creation
14167           escape();
14168         } else {
14169           endPolylineSegment();
14170           float [][] points = this.newPolyline.getPoints();
14171           // If last edited point matches first point validate drawn polyline
14172           if (points.length > 2
14173               && this.newPolyline.getPointIndexAt(points [points.length - 1][0], points [points.length - 1][1], 0.001f) == 0) {
14174             // Remove last currently edited point and close path
14175             this.newPolyline.removePoint(this.newPolyline.getPointCount() - 1);
14176             this.newPolyline.setClosedPath(true);
14177             validateDrawnPolyline();
14178             return;
14179           }
14180           createNextSegment();
14181           // Reactivate automatically second step
14182           planView.deleteToolTipFeedback();
14183           setEditionActivated(true);
14184         }
14185       }
14186     }
14187 
14188     private void createNextSegment() {
14189       // Add a point to current polyline
14190       float [][] points = this.newPolyline.getPoints();
14191       this.xPreviousPoint = points [points.length - 1][0];
14192       this.yPreviousPoint = points [points.length - 1][1];
14193       // Create a new segment with an angle equal to previous segment angle - 90�
14194       double previousSegmentAngle = Math.PI - Math.atan2(points [points.length - 2][1] - points [points.length - 1][1],
14195           points [points.length - 2][0] - points [points.length - 1][0]);
14196       previousSegmentAngle -=  Math.PI / 2;
14197       float previousSegmentLength = getPolylineSegmentLength(this.newPolyline, points.length - 1);
14198       this.newPolyline.addPoint(
14199           (float)(this.xPreviousPoint + previousSegmentLength * Math.cos(previousSegmentAngle)),
14200           (float)(this.yPreviousPoint - previousSegmentLength * Math.sin(previousSegmentAngle)));
14201       this.newPoint = null;
14202       this.lastPointCreationTime = System.currentTimeMillis();
14203     }
14204 
14205     @Override
14206     public void updateEditableProperty(EditableProperty editableProperty, Object value) {
14207       PlanView planView = getView();
14208       if (this.newPolyline == null) {
14209         // Update start point of the first wall
14210         switch (editableProperty) {
14211           case X :
14212             this.xPreviousPoint = value != null ? ((Number)value).floatValue() : 0;
14213             this.xPreviousPoint = Math.max(-100000f, Math.min(this.xPreviousPoint, 100000f));
14214             break;
14215           case Y :
14216             this.yPreviousPoint = value != null ? ((Number)value).floatValue() : 0;
14217             this.yPreviousPoint = Math.max(-100000f, Math.min(this.yPreviousPoint, 100000f));
14218             break;
14219         }
14220         planView.setAlignmentFeedback(Polyline.class, null, this.xPreviousPoint, this.yPreviousPoint, true);
14221         planView.makePointVisible(this.xPreviousPoint, this.yPreviousPoint);
14222       } else {
14223         float [][] points = this.newPolyline.getPoints();
14224         float [] previousPoint = points [points.length - 2];
14225         float [] point = points [points.length - 1];
14226         float newX;
14227         float newY;
14228         // Update end point of the current polyline
14229         switch (editableProperty) {
14230           case LENGTH :
14231             float length = value != null ? ((Number)value).floatValue() : 0;
14232             length = Math.max(0.001f, Math.min(length, preferences.getLengthUnit().getMaximumLength()));
14233             double segmentAngle = Math.PI - Math.atan2(previousPoint [1] - point [1],
14234                 previousPoint [0] - point [0]);
14235             newX = (float)(previousPoint [0] + length * Math.cos(segmentAngle));
14236             newY = (float)(previousPoint [1] - length * Math.sin(segmentAngle));
14237             break;
14238           case ANGLE :
14239             segmentAngle = Math.toRadians(value != null ? ((Number)value).floatValue() : 0);
14240             if (points.length > 2) {
14241               segmentAngle -= Math.atan2(points [points.length - 3][1] - previousPoint [1],
14242                   points [points.length - 3][0] - previousPoint [0]);
14243             }
14244             float segmentLength = getPolylineSegmentLength(this.newPolyline, points.length - 1);
14245             newX = (float)(previousPoint [0] + segmentLength * Math.cos(segmentAngle));
14246             newY = (float)(previousPoint [1] - segmentLength * Math.sin(segmentAngle));
14247             break;
14248           default :
14249             return;
14250         }
14251         this.newPolyline.setPoint(newX, newY, points.length - 1);
14252 
14253         if (this.newPolyline.getJoinStyle() != Polyline.JoinStyle.CURVED) {
14254           showPolylineAngleFeedback(this.newPolyline, points.length - 1);
14255         }
14256         planView.setAlignmentFeedback(Polyline.class, null, newX, newY, false);
14257         // Ensure polyline segment points are visible
14258         planView.makePointVisible(points [points.length - 2][0], points [points.length - 2][1]);
14259         planView.makePointVisible(points [points.length - 1][0], points [points.length - 1][1]);
14260       }
14261     }
14262 
14263     @Override
14264     public void toggleMagnetism(boolean magnetismToggled) {
14265       // Compute active magnetism
14266       this.magnetismEnabled = preferences.isMagnetismEnabled()
14267                               ^ magnetismToggled;
14268       if (this.newPolyline != null) {
14269         moveMouse(getXLastMouseMove(), getYLastMouseMove());
14270       }
14271     }
14272 
14273     @Override
14274     public void setAlignmentActivated(boolean alignmentActivated) {
14275       this.alignmentActivated = alignmentActivated;
14276       moveMouse(getXLastMouseMove(), getYLastMouseMove());
14277     }
14278 
14279     @Override
14280     public void setDuplicationActivated(boolean duplicationActivated) {
14281       // Reuse duplication activation for curved polyline creation
14282       this.curvedPolyline = duplicationActivated;
14283     }
14284 
14285     @Override
14286     public void escape() {
14287       if (this.newPolyline != null
14288           && this.newPoint == null) {
14289         // Remove last currently edited point.
14290         this.newPolyline.removePoint(this.newPolyline.getPointCount() - 1);
14291       }
14292       validateDrawnPolyline();
14293     }
14294 
14295     @Override
14296     public void exit() {
14297       getView().deleteFeedback();
14298       this.newPolyline = null;
14299       this.newPoint = null;
14300       this.oldSelection = null;
14301     }
14302   }
14303 
14304   /**
14305    * Polyline resize state. This state manages polyline resizing.
14306    */
14307   private class PolylineResizeState extends AbstractPolylineState {
14308     private Collection<Polyline> polylines;
14309     private Polyline             selectedPolyline;
14310     private int                  polylinePointIndex;
14311     private float                oldX;
14312     private float                oldY;
14313     private float                deltaXToResizePoint;
14314     private float                deltaYToResizePoint;
14315     private boolean              magnetismEnabled;
14316     private boolean              alignmentActivated;
14317 
14318     @Override
14319     public Mode getMode() {
14320       return Mode.SELECTION;
14321     }
14322 
14323     @Override
14324     public boolean isModificationState() {
14325       return true;
14326     }
14327 
14328     @Override
14329     public boolean isBasePlanModificationState() {
14330       return true;
14331     }
14332 
14333     @Override
14334     public void enter() {
14335       super.enter();
14336       this.selectedPolyline = (Polyline)home.getSelectedItems().get(0);
14337       this.polylines = new ArrayList<Polyline>(home.getPolylines());
14338       this.polylines.remove(this.selectedPolyline);
14339       float margin = getIndicatorMargin();
14340       this.polylinePointIndex = this.selectedPolyline.getPointIndexAt(
14341           getXLastMousePress(), getYLastMousePress(), margin);
14342       float [][] polylinePoints = this.selectedPolyline.getPoints();
14343       this.oldX = polylinePoints [this.polylinePointIndex][0];
14344       this.oldY = polylinePoints [this.polylinePointIndex][1];
14345       this.deltaXToResizePoint = getXLastMousePress() - this.oldX;
14346       this.deltaYToResizePoint = getYLastMousePress() - this.oldY;
14347       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
14348       toggleMagnetism(wasMagnetismToggledLastMousePress());
14349       PlanView planView = getView();
14350       planView.setResizeIndicatorVisible(true);
14351       String toolTipFeedbackText = getToolTipFeedbackText(this.selectedPolyline, this.polylinePointIndex);
14352       if (toolTipFeedbackText != null) {
14353         planView.setToolTipFeedback(toolTipFeedbackText, getXLastMousePress(), getYLastMousePress());
14354         if (this.selectedPolyline.getJoinStyle() != Polyline.JoinStyle.CURVED) {
14355           showPolylineAngleFeedback(this.selectedPolyline, this.polylinePointIndex);
14356         }
14357       }
14358     }
14359 
14360     @Override
14361     public void moveMouse(float x, float y) {
14362       PlanView planView = getView();
14363       float newX = x - this.deltaXToResizePoint;
14364       float newY = y - this.deltaYToResizePoint;
14365       if (this.alignmentActivated
14366           || this.magnetismEnabled) {
14367         float [][] polylinePoints = this.selectedPolyline.getPoints();
14368         int previousPointIndex = this.polylinePointIndex == 0
14369             ? (this.selectedPolyline.isClosedPath()
14370                   ? polylinePoints.length - 1
14371                   : 1)
14372             : this.polylinePointIndex - 1;
14373         float xPreviousPoint = polylinePoints [previousPointIndex][0];
14374         float yPreviousPoint = polylinePoints [previousPointIndex][1];
14375         PointWithAngleMagnetism pointWithAngleMagnetism = new PointWithAngleMagnetism(
14376             xPreviousPoint, yPreviousPoint, newX, newY, preferences.getLengthUnit(), planView.getPixelLength());
14377         newX = pointWithAngleMagnetism.getX();
14378         newY = pointWithAngleMagnetism.getY();
14379       }
14380       this.selectedPolyline.setPoint(newX, newY, this.polylinePointIndex);
14381 
14382       planView.setToolTipFeedback(getToolTipFeedbackText(this.selectedPolyline, this.polylinePointIndex), x, y);
14383       if (this.selectedPolyline.getJoinStyle() != Polyline.JoinStyle.CURVED) {
14384         showPolylineAngleFeedback(this.selectedPolyline, this.polylinePointIndex);
14385       }
14386       // Ensure point at (x,y) is visible
14387       planView.makePointVisible(x, y);
14388     }
14389 
14390     @Override
14391     public void releaseMouse(float x, float y) {
14392       postPolylineResize(this.selectedPolyline, this.oldX, this.oldY, this.polylinePointIndex);
14393       setState(getSelectionState());
14394     }
14395 
14396     @Override
14397     public void toggleMagnetism(boolean magnetismToggled) {
14398       // Compute active magnetism
14399       this.magnetismEnabled = preferences.isMagnetismEnabled()
14400                               ^ magnetismToggled;
14401       moveMouse(getXLastMouseMove(), getYLastMouseMove());
14402     }
14403 
14404     @Override
14405     public void setAlignmentActivated(boolean alignmentActivated) {
14406       this.alignmentActivated = alignmentActivated;
14407       moveMouse(getXLastMouseMove(), getYLastMouseMove());
14408     }
14409 
14410     @Override
14411     public void escape() {
14412       this.selectedPolyline.setPoint(this.oldX, this.oldY, this.polylinePointIndex);
14413       setState(getSelectionState());
14414     }
14415 
14416     @Override
14417     public void exit() {
14418       PlanView planView = getView();
14419       planView.setResizeIndicatorVisible(false);
14420       planView.deleteFeedback();
14421       this.selectedPolyline = null;
14422     }
14423   }
14424 
14425   /**
14426    * Label creation state. This state manages transition to
14427    * other modes, and initial label creation.
14428    */
14429   private class LabelCreationState extends AbstractModeChangeState {
14430     @Override
14431     public Mode getMode() {
14432       return Mode.LABEL_CREATION;
14433     }
14434 
14435     @Override
14436     public void enter() {
14437       getView().setCursor(PlanView.CursorType.DRAW);
14438     }
14439 
14440     @Override
14441     public void pressMouse(float x, float y, int clickCount,
14442                            boolean shiftDown, boolean duplicationActivated) {
14443       createLabel(x, y);
14444       if (getPointerTypeLastMousePress() == View.PointerType.TOUCH) {
14445         setState(getSelectionState());
14446       }
14447     }
14448   }
14449 
14450   /**
14451    * Label rotation state. This state manages the rotation of a label.
14452    */
14453   private class LabelRotationState extends ControllerState {
14454     private static final int STEP_COUNT = 24;
14455 
14456     private Label    selectedLabel;
14457     private float    oldAngle;
14458     private float    angleMousePress;
14459     private boolean  magnetismEnabled;
14460     private boolean  alignmentActivated;
14461 
14462     @Override
14463     public Mode getMode() {
14464       return Mode.SELECTION;
14465     }
14466 
14467     @Override
14468     public boolean isModificationState() {
14469       return true;
14470     }
14471 
14472     @Override
14473     public boolean isBasePlanModificationState() {
14474       return true;
14475     }
14476 
14477     @Override
14478     public void enter() {
14479       this.selectedLabel = (Label)home.getSelectedItems().get(0);
14480       this.angleMousePress = (float)Math.atan2(this.selectedLabel.getY() - getYLastMousePress(),
14481           getXLastMousePress() - this.selectedLabel.getX());
14482       this.oldAngle = this.selectedLabel.getAngle();
14483       this.alignmentActivated = wasAlignmentActivatedLastMousePress();
14484       this.magnetismEnabled = preferences.isMagnetismEnabled()
14485           ^ wasMagnetismToggledLastMousePress();
14486       PlanView planView = getView();
14487       planView.setResizeIndicatorVisible(true);
14488     }
14489 
14490     @Override
14491     public void moveMouse(float x, float y) {
14492       if (x != this.selectedLabel.getX() || y != this.selectedLabel.getY()) {
14493         // Compute the new angle of the label text
14494         float angleMouseMove = (float)Math.atan2(this.selectedLabel.getY() - y,
14495             x - this.selectedLabel.getX());
14496         float newAngle = this.oldAngle - angleMouseMove + this.angleMousePress;
14497 
14498         if (this.alignmentActivated
14499             ||this.magnetismEnabled) {
14500           float angleStep = 2 * (float)Math.PI / STEP_COUNT;
14501           // Compute angles closest to a step angle (multiple of angleStep)
14502           newAngle = Math.round(newAngle / angleStep) * angleStep;
14503         }
14504 
14505         // Update label text new angle
14506         this.selectedLabel.setAngle(newAngle);
14507         getView().makePointVisible(x, y);
14508       }
14509     }
14510 
14511     @Override
14512     public void releaseMouse(float x, float y) {
14513       postLabelRotation(this.selectedLabel, this.oldAngle);
14514       setState(getSelectionState());
14515     }
14516 
14517     @Override
14518     public void toggleMagnetism(boolean magnetismToggled) {
14519       // Compute active magnetism
14520       this.magnetismEnabled = preferences.isMagnetismEnabled()
14521                               ^ magnetismToggled;
14522       // Compute again angle as if mouse moved
14523       moveMouse(getXLastMouseMove(), getYLastMouseMove());
14524     }
14525 
14526     @Override
14527     public void setAlignmentActivated(boolean alignmentActivated) {
14528       this.alignmentActivated = alignmentActivated;
14529       moveMouse(getXLastMouseMove(), getYLastMouseMove());
14530     }
14531 
14532     @Override
14533     public void escape() {
14534       this.selectedLabel.setAngle(this.oldAngle);
14535       setState(getSelectionState());
14536     }
14537 
14538     @Override
14539     public void exit() {
14540       getView().setResizeIndicatorVisible(false);
14541       this.selectedLabel = null;
14542     }
14543   }
14544 
14545   /**
14546    * Label elevation state. This states manages the elevation change of a label.
14547    */
14548   private class LabelElevationState extends ControllerState {
14549     private boolean magnetismEnabled;
14550     private float   deltaYToElevationPoint;
14551     private Label   selectedLabel;
14552     private float   oldElevation;
14553     private String  elevationToolTipFeedback;
14554 
14555     @Override
14556     public Mode getMode() {
14557       return Mode.SELECTION;
14558     }
14559 
14560     @Override
14561     public boolean isModificationState() {
14562       return true;
14563     }
14564 
14565     @Override
14566     public boolean isBasePlanModificationState() {
14567       return true;
14568     }
14569 
14570     @Override
14571     public void enter() {
14572       this.elevationToolTipFeedback = preferences.getLocalizedString(
14573           PlanController.class, "elevationToolTipFeedback");
14574       this.selectedLabel = (Label)home.getSelectedItems().get(0);
14575       TextStyle textStyle = getItemTextStyle(this.selectedLabel, this.selectedLabel.getStyle());
14576       float [][] textBounds = getView().getTextBounds(this.selectedLabel.getText(), textStyle,
14577           this.selectedLabel.getX(), this.selectedLabel.getY(), this.selectedLabel.getAngle());
14578       this.deltaYToElevationPoint = getYLastMousePress() - (textBounds [2][1] + textBounds [3][1]) / 2;
14579       this.oldElevation = this.selectedLabel.getElevation();
14580       this.magnetismEnabled = preferences.isMagnetismEnabled()
14581                               ^ wasMagnetismToggledLastMousePress();
14582       PlanView planView = getView();
14583       planView.setResizeIndicatorVisible(true);
14584       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldElevation),
14585           getXLastMousePress(), getYLastMousePress());
14586     }
14587 
14588     @Override
14589     public void moveMouse(float x, float y) {
14590       // Compute the new elevation of the piece
14591       PlanView planView = getView();
14592       TextStyle textStyle = getItemTextStyle(this.selectedLabel, this.selectedLabel.getStyle());
14593       float [][] textBounds = getView().getTextBounds(this.selectedLabel.getText(), textStyle,
14594           this.selectedLabel.getX(), this.selectedLabel.getY(), this.selectedLabel.getAngle());
14595       float deltaY = y - this.deltaYToElevationPoint - (textBounds [2][1] + textBounds [3][1]) / 2;
14596       float newElevation = this.oldElevation - deltaY;
14597       newElevation = Math.min(Math.max(newElevation, 0f), preferences.getLengthUnit().getMaximumElevation());
14598       if (this.magnetismEnabled) {
14599         newElevation = preferences.getLengthUnit().getMagnetizedLength(newElevation, planView.getPixelLength());
14600       }
14601 
14602       // Update piece new dimension
14603       this.selectedLabel.setElevation(newElevation);
14604 
14605       // Ensure point at (x,y) is visible
14606       planView.makePointVisible(x, y);
14607       planView.setToolTipFeedback(getToolTipFeedbackText(newElevation), x, y);
14608     }
14609 
14610     @Override
14611     public void releaseMouse(float x, float y) {
14612       postLabelElevation(this.selectedLabel, this.oldElevation);
14613       setState(getSelectionState());
14614     }
14615 
14616     @Override
14617     public void toggleMagnetism(boolean magnetismToggled) {
14618       // Compute active magnetism
14619       this.magnetismEnabled = preferences.isMagnetismEnabled()
14620                               ^ magnetismToggled;
14621       // Compute again angle as if mouse moved
14622       moveMouse(getXLastMouseMove(), getYLastMouseMove());
14623     }
14624 
14625     @Override
14626     public void escape() {
14627       this.selectedLabel.setElevation(this.oldElevation);
14628       setState(getSelectionState());
14629     }
14630 
14631     @Override
14632     public void exit() {
14633       PlanView planView = getView();
14634       planView.setResizeIndicatorVisible(false);
14635       planView.deleteFeedback();
14636       this.selectedLabel = null;
14637     }
14638 
14639     private String getToolTipFeedbackText(float height) {
14640       return String.format(this.elevationToolTipFeedback,
14641           preferences.getLengthUnit().getFormatWithUnit().format(height));
14642     }
14643   }
14644 
14645   /**
14646    * Compass rotation state. This states manages the rotation of the compass.
14647    */
14648   private class CompassRotationState extends ControllerState {
14649     private Compass selectedCompass;
14650     private float   angleMousePress;
14651     private float   oldNorthDirection;
14652     private String  rotationToolTipFeedback;
14653 
14654     @Override
14655     public Mode getMode() {
14656       return Mode.SELECTION;
14657     }
14658 
14659     @Override
14660     public boolean isModificationState() {
14661       return true;
14662     }
14663 
14664     @Override
14665     public boolean isBasePlanModificationState() {
14666       return true;
14667     }
14668 
14669     @Override
14670     public void enter() {
14671       this.rotationToolTipFeedback = preferences.getLocalizedString(
14672           PlanController.class, "rotationToolTipFeedback");
14673       this.selectedCompass = (Compass)home.getSelectedItems().get(0);
14674       this.angleMousePress = (float)Math.atan2(this.selectedCompass.getY() - getYLastMousePress(),
14675           getXLastMousePress() - this.selectedCompass.getX());
14676       this.oldNorthDirection = this.selectedCompass.getNorthDirection();
14677       PlanView planView = getView();
14678       planView.setResizeIndicatorVisible(true);
14679       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldNorthDirection),
14680           getXLastMousePress(), getYLastMousePress());
14681     }
14682 
14683     @Override
14684     public void moveMouse(float x, float y) {
14685       if (x != this.selectedCompass.getX() || y != this.selectedCompass.getY()) {
14686         // Compute the new north direction of the compass
14687         float angleMouseMove = (float)Math.atan2(this.selectedCompass.getY() - y,
14688             x - this.selectedCompass.getX());
14689         float newNorthDirection = this.oldNorthDirection - angleMouseMove + this.angleMousePress;
14690         float angleStep = (float)Math.PI / 180;
14691         // Compute angles closest to a degree with a value between 0 and 2 PI
14692         newNorthDirection = Math.round(newNorthDirection / angleStep) * angleStep;
14693         newNorthDirection = (float)((newNorthDirection +  2 * Math.PI) % (2 * Math.PI));
14694         // Update compass new north direction
14695         this.selectedCompass.setNorthDirection(newNorthDirection);
14696         // Ensure point at (x,y) is visible
14697         PlanView planView = getView();
14698         planView.makePointVisible(x, y);
14699         planView.setToolTipFeedback(getToolTipFeedbackText(newNorthDirection), x, y);
14700       }
14701     }
14702 
14703     @Override
14704     public void releaseMouse(float x, float y) {
14705       postCompassRotation(this.selectedCompass, this.oldNorthDirection);
14706       setState(getSelectionState());
14707     }
14708 
14709     @Override
14710     public void escape() {
14711       this.selectedCompass.setNorthDirection(this.oldNorthDirection);
14712       setState(getSelectionState());
14713     }
14714 
14715     @Override
14716     public void exit() {
14717       PlanView planView = getView();
14718       planView.setResizeIndicatorVisible(false);
14719       planView.deleteFeedback();
14720       this.selectedCompass = null;
14721     }
14722 
14723     private String getToolTipFeedbackText(float angle) {
14724       return String.format(this.rotationToolTipFeedback, Math.round(Math.toDegrees(angle)));
14725     }
14726   }
14727 
14728   /**
14729    * Compass resize state. This states manages the resizing of the compass.
14730    */
14731   private class CompassResizeState extends ControllerState {
14732     private Compass  selectedCompass;
14733     private float    oldDiameter;
14734     private float    deltaXToResizePoint;
14735     private float    deltaYToResizePoint;
14736     private String   resizeToolTipFeedback;
14737 
14738     @Override
14739     public Mode getMode() {
14740       return Mode.SELECTION;
14741     }
14742 
14743     @Override
14744     public boolean isModificationState() {
14745       return true;
14746     }
14747 
14748     @Override
14749     public boolean isBasePlanModificationState() {
14750       return true;
14751     }
14752 
14753     @Override
14754     public void enter() {
14755       this.resizeToolTipFeedback = preferences.getLocalizedString(
14756           PlanController.class, "diameterToolTipFeedback");
14757       this.selectedCompass = (Compass)home.getSelectedItems().get(0);
14758       float [][] compassPoints = this.selectedCompass.getPoints();
14759       float xMiddleSecondAndThirdPoint = (compassPoints [1][0] + compassPoints [2][0]) / 2;
14760       float yMiddleSecondAndThirdPoint = (compassPoints [1][1] + compassPoints [2][1]) / 2;
14761       this.deltaXToResizePoint = getXLastMousePress() - xMiddleSecondAndThirdPoint;
14762       this.deltaYToResizePoint = getYLastMousePress() - yMiddleSecondAndThirdPoint;
14763       this.oldDiameter = this.selectedCompass.getDiameter();
14764       PlanView planView = getView();
14765       planView.setResizeIndicatorVisible(true);
14766       planView.setToolTipFeedback(getToolTipFeedbackText(this.oldDiameter),
14767           getXLastMousePress(), getYLastMousePress());
14768     }
14769 
14770     @Override
14771     public void moveMouse(float x, float y) {
14772       // Compute the new diameter of the compass
14773       PlanView planView = getView();
14774       float newDiameter = (float)Point2D.distance(this.selectedCompass.getX(), this.selectedCompass.getY(),
14775           x - this.deltaXToResizePoint, y - this.deltaYToResizePoint) * 2;
14776       newDiameter = preferences.getLengthUnit().getMagnetizedLength(newDiameter, planView.getPixelLength());
14777       newDiameter = Math.min(Math.max(newDiameter, preferences.getLengthUnit().getMinimumLength()),
14778           preferences.getLengthUnit().getMaximumLength() / 10);
14779       // Update piece size
14780       this.selectedCompass.setDiameter(newDiameter);
14781       // Ensure point at (x,y) is visible
14782       planView.makePointVisible(x, y);
14783       planView.setToolTipFeedback(getToolTipFeedbackText(newDiameter), x, y);
14784     }
14785 
14786     @Override
14787     public void releaseMouse(float x, float y) {
14788       postCompassResize(this.selectedCompass, this.oldDiameter);
14789       setState(getSelectionState());
14790     }
14791 
14792     @Override
14793     public void escape() {
14794       this.selectedCompass.setDiameter(this.oldDiameter);
14795       setState(getSelectionState());
14796     }
14797 
14798     @Override
14799     public void exit() {
14800       PlanView planView = getView();
14801       planView.setResizeIndicatorVisible(false);
14802       planView.deleteFeedback();
14803       this.selectedCompass = null;
14804     }
14805 
14806     private String getToolTipFeedbackText(float diameter) {
14807       return String.format(this.resizeToolTipFeedback,
14808           preferences.getLengthUnit().getFormatWithUnit().format(diameter));
14809     }
14810   }
14811 }
14812