1 /*
2  * PlanComponent.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.swing;
21 
22 import java.awt.AWTKeyStroke;
23 import java.awt.AlphaComposite;
24 import java.awt.BasicStroke;
25 import java.awt.Color;
26 import java.awt.Component;
27 import java.awt.Composite;
28 import java.awt.Container;
29 import java.awt.Cursor;
30 import java.awt.Dimension;
31 import java.awt.EventQueue;
32 import java.awt.FlowLayout;
33 import java.awt.Font;
34 import java.awt.FontMetrics;
35 import java.awt.Graphics;
36 import java.awt.Graphics2D;
37 import java.awt.GridBagConstraints;
38 import java.awt.GridBagLayout;
39 import java.awt.Insets;
40 import java.awt.KeyEventPostProcessor;
41 import java.awt.KeyboardFocusManager;
42 import java.awt.MouseInfo;
43 import java.awt.Paint;
44 import java.awt.Point;
45 import java.awt.Rectangle;
46 import java.awt.RenderingHints;
47 import java.awt.Shape;
48 import java.awt.Stroke;
49 import java.awt.TexturePaint;
50 import java.awt.Toolkit;
51 import java.awt.Window;
52 import java.awt.dnd.DragSource;
53 import java.awt.event.ActionEvent;
54 import java.awt.event.FocusAdapter;
55 import java.awt.event.FocusEvent;
56 import java.awt.event.KeyEvent;
57 import java.awt.event.KeyListener;
58 import java.awt.event.MouseEvent;
59 import java.awt.event.MouseWheelEvent;
60 import java.awt.event.MouseWheelListener;
61 import java.awt.font.FontRenderContext;
62 import java.awt.font.TextLayout;
63 import java.awt.geom.AffineTransform;
64 import java.awt.geom.Arc2D;
65 import java.awt.geom.Area;
66 import java.awt.geom.Ellipse2D;
67 import java.awt.geom.GeneralPath;
68 import java.awt.geom.Line2D;
69 import java.awt.geom.PathIterator;
70 import java.awt.geom.Point2D;
71 import java.awt.geom.Rectangle2D;
72 import java.awt.image.BufferedImage;
73 import java.awt.image.FilteredImageSource;
74 import java.awt.image.MemoryImageSource;
75 import java.awt.image.RGBImageFilter;
76 import java.awt.print.PageFormat;
77 import java.awt.print.Printable;
78 import java.beans.PropertyChangeEvent;
79 import java.beans.PropertyChangeListener;
80 import java.io.IOException;
81 import java.io.InputStream;
82 import java.io.InterruptedIOException;
83 import java.io.OutputStream;
84 import java.lang.ref.WeakReference;
85 import java.lang.reflect.Method;
86 import java.net.URL;
87 import java.security.AccessControlException;
88 import java.text.DecimalFormat;
89 import java.text.Format;
90 import java.text.NumberFormat;
91 import java.text.ParseException;
92 import java.util.ArrayList;
93 import java.util.Arrays;
94 import java.util.Collection;
95 import java.util.Collections;
96 import java.util.Comparator;
97 import java.util.Enumeration;
98 import java.util.HashMap;
99 import java.util.HashSet;
100 import java.util.IdentityHashMap;
101 import java.util.Iterator;
102 import java.util.LinkedHashMap;
103 import java.util.List;
104 import java.util.Locale;
105 import java.util.Map;
106 import java.util.Properties;
107 import java.util.Set;
108 import java.util.WeakHashMap;
109 import java.util.concurrent.ExecutorService;
110 import java.util.concurrent.Executors;
111 
112 import javax.imageio.ImageIO;
113 import javax.media.j3d.AmbientLight;
114 import javax.media.j3d.Appearance;
115 import javax.media.j3d.Background;
116 import javax.media.j3d.BoundingBox;
117 import javax.media.j3d.BranchGroup;
118 import javax.media.j3d.Canvas3D;
119 import javax.media.j3d.DirectionalLight;
120 import javax.media.j3d.Group;
121 import javax.media.j3d.ImageComponent2D;
122 import javax.media.j3d.Light;
123 import javax.media.j3d.Link;
124 import javax.media.j3d.Node;
125 import javax.media.j3d.Shape3D;
126 import javax.media.j3d.Texture;
127 import javax.media.j3d.Transform3D;
128 import javax.media.j3d.TransformGroup;
129 import javax.swing.AbstractAction;
130 import javax.swing.Action;
131 import javax.swing.ActionMap;
132 import javax.swing.BorderFactory;
133 import javax.swing.Icon;
134 import javax.swing.ImageIcon;
135 import javax.swing.InputMap;
136 import javax.swing.JApplet;
137 import javax.swing.JComponent;
138 import javax.swing.JFormattedTextField;
139 import javax.swing.JLabel;
140 import javax.swing.JOptionPane;
141 import javax.swing.JPanel;
142 import javax.swing.JToolTip;
143 import javax.swing.JViewport;
144 import javax.swing.JWindow;
145 import javax.swing.KeyStroke;
146 import javax.swing.Scrollable;
147 import javax.swing.SwingConstants;
148 import javax.swing.SwingUtilities;
149 import javax.swing.UIManager;
150 import javax.swing.border.Border;
151 import javax.swing.event.AncestorEvent;
152 import javax.swing.event.AncestorListener;
153 import javax.swing.event.ChangeEvent;
154 import javax.swing.event.ChangeListener;
155 import javax.swing.event.DocumentEvent;
156 import javax.swing.event.DocumentListener;
157 import javax.swing.event.MouseInputAdapter;
158 import javax.swing.event.MouseInputListener;
159 import javax.swing.text.DefaultFormatterFactory;
160 import javax.swing.text.InternationalFormatter;
161 import javax.swing.text.NumberFormatter;
162 import javax.vecmath.Color3f;
163 import javax.vecmath.Point3d;
164 import javax.vecmath.Vector3d;
165 import javax.vecmath.Vector3f;
166 
167 import org.freehep.graphicsio.ImageConstants;
168 import org.freehep.graphicsio.svg.SVGGraphics2D;
169 import org.freehep.util.UserProperties;
170 
171 import com.eteks.sweethome3d.j3d.Component3DManager;
172 import com.eteks.sweethome3d.j3d.ModelManager;
173 import com.eteks.sweethome3d.j3d.Object3DBranch;
174 import com.eteks.sweethome3d.j3d.Object3DBranchFactory;
175 import com.eteks.sweethome3d.j3d.ShapeTools;
176 import com.eteks.sweethome3d.j3d.TextureManager;
177 import com.eteks.sweethome3d.model.BackgroundImage;
178 import com.eteks.sweethome3d.model.Camera;
179 import com.eteks.sweethome3d.model.CollectionEvent;
180 import com.eteks.sweethome3d.model.CollectionListener;
181 import com.eteks.sweethome3d.model.Compass;
182 import com.eteks.sweethome3d.model.Content;
183 import com.eteks.sweethome3d.model.DimensionLine;
184 import com.eteks.sweethome3d.model.Elevatable;
185 import com.eteks.sweethome3d.model.Home;
186 import com.eteks.sweethome3d.model.HomeDoorOrWindow;
187 import com.eteks.sweethome3d.model.HomeFurnitureGroup;
188 import com.eteks.sweethome3d.model.HomeLight;
189 import com.eteks.sweethome3d.model.HomePieceOfFurniture;
190 import com.eteks.sweethome3d.model.HomeTexture;
191 import com.eteks.sweethome3d.model.Label;
192 import com.eteks.sweethome3d.model.LengthUnit;
193 import com.eteks.sweethome3d.model.Level;
194 import com.eteks.sweethome3d.model.ObserverCamera;
195 import com.eteks.sweethome3d.model.PieceOfFurniture;
196 import com.eteks.sweethome3d.model.Polyline;
197 import com.eteks.sweethome3d.model.Room;
198 import com.eteks.sweethome3d.model.Sash;
199 import com.eteks.sweethome3d.model.Selectable;
200 import com.eteks.sweethome3d.model.SelectionEvent;
201 import com.eteks.sweethome3d.model.SelectionListener;
202 import com.eteks.sweethome3d.model.TextStyle;
203 import com.eteks.sweethome3d.model.TextureImage;
204 import com.eteks.sweethome3d.model.UserPreferences;
205 import com.eteks.sweethome3d.model.Wall;
206 import com.eteks.sweethome3d.tools.OperatingSystem;
207 import com.eteks.sweethome3d.viewcontroller.Object3DFactory;
208 import com.eteks.sweethome3d.viewcontroller.PlanController;
209 import com.eteks.sweethome3d.viewcontroller.PlanView;
210 import com.eteks.sweethome3d.viewcontroller.View;
211 import com.sun.j3d.utils.universe.SimpleUniverse;
212 import com.sun.j3d.utils.universe.Viewer;
213 import com.sun.j3d.utils.universe.ViewingPlatform;
214 
215 /**
216  * A component displaying the plan of a home.
217  * @author Emmanuel Puybaret
218  */
219 public class PlanComponent extends JComponent implements PlanView, Scrollable, Printable {
220   /**
221    * The circumstances under which the home items displayed by this component will be painted.
222    */
223   protected enum PaintMode {PAINT, PRINT, CLIPBOARD, EXPORT}
224 
225   private enum ActionType {DELETE_SELECTION, ESCAPE,
226       MOVE_SELECTION_LEFT, MOVE_SELECTION_UP, MOVE_SELECTION_DOWN, MOVE_SELECTION_RIGHT,
227       MOVE_SELECTION_FAST_LEFT, MOVE_SELECTION_FAST_UP, MOVE_SELECTION_FAST_DOWN, MOVE_SELECTION_FAST_RIGHT,
228       TOGGLE_MAGNETISM_ON, TOGGLE_MAGNETISM_OFF,
229       ACTIVATE_ALIGNMENT, DEACTIVATE_ALIGNMENT,
230       ACTIVATE_DUPLICATION, DEACTIVATE_DUPLICATION,
231       ACTIVATE_EDITIION, DEACTIVATE_EDITIION}
232 
233   /**
234    * Indicator types that may be displayed on selected items.
235    */
236   public static class IndicatorType {
237     // Don't qualify IndicatorType as an enumeration to be able to extend IndicatorType class
238     public static final IndicatorType ROTATE        = new IndicatorType("ROTATE");
239     public static final IndicatorType RESIZE        = new IndicatorType("RESIZE");
240     public static final IndicatorType ELEVATE       = new IndicatorType("ELEVATE");
241     public static final IndicatorType RESIZE_HEIGHT = new IndicatorType("RESIZE_HEIGHT");
242     public static final IndicatorType CHANGE_POWER  = new IndicatorType("CHANGE_POWER");
243     public static final IndicatorType MOVE_TEXT     = new IndicatorType("MOVE_TEXT");
244     public static final IndicatorType ROTATE_TEXT   = new IndicatorType("ROTATE_TEXT");
245     public static final IndicatorType ROTATE_PITCH  = new IndicatorType("ROTATE_PITCH");
246     public static final IndicatorType ROTATE_ROLL   = new IndicatorType("ROTATE_ROLL");
247     public static final IndicatorType ARC_EXTENT    = new IndicatorType("ARC_EXTENT");
248 
249     private final String name;
250 
IndicatorType(String name)251     protected IndicatorType(String name) {
252       this.name = name;
253     }
254 
name()255     public final String name() {
256       return this.name;
257     }
258 
259     @Override
toString()260     public String toString() {
261       return this.name;
262     }
263   };
264 
265   private static final float    MARGIN = 40;
266 
267   private final Home            home;
268   private final UserPreferences preferences;
269   private final Object3DFactory object3dFactory;
270   private float                 resolutionScale = SwingTools.getResolutionScale();
271   private float                 scale = 0.5f;
272   private boolean               selectedItemsOutlinePainted = true;
273   private boolean               backgroundPainted = true;
274 
275   private PlanRulerComponent    horizontalRuler;
276   private PlanRulerComponent    verticalRuler;
277 
278   private final Cursor          rotationCursor;
279   private final Cursor          elevationCursor;
280   private final Cursor          heightCursor;
281   private final Cursor          powerCursor;
282   private final Cursor          resizeCursor;
283   private final Cursor          moveCursor;
284   private final Cursor          panningCursor;
285   private final Cursor          duplicationCursor;
286 
287   private Rectangle2D           rectangleFeedback;
288   private Class<? extends Selectable> alignedObjectClass;
289   private Selectable            alignedObjectFeedback;
290   private Point2D               locationFeeback;
291   private boolean               showPointFeedback;
292   private Point2D               centerAngleFeedback;
293   private Point2D               point1AngleFeedback;
294   private Point2D               point2AngleFeedback;
295   private List<Selectable>      draggedItemsFeedback;
296   private List<DimensionLine>   dimensionLinesFeedback;
297   private boolean               selectionScrollUpdated;
298   private boolean               wallsDoorsOrWindowsModification;
299   private JToolTip              toolTip;
300   private JWindow               toolTipWindow;
301   private boolean               resizeIndicatorVisible;
302 
303   private Map<PlanController.EditableProperty, JFormattedTextField> toolTipEditableTextFields;
304   private KeyListener                       toolTipKeyListener;
305   private KeyEventPostProcessor             windowsAltPostProcessor;
306 
307   private List<HomePieceOfFurniture>        sortedLevelFurniture;
308   private List<Room>                        sortedLevelRooms;
309   private Map<TextStyle, Font>              fonts;
310   private Map<TextStyle, FontMetrics>       fontsMetrics;
311 
312   private Rectangle2D                       planBoundsCache;
313   private boolean                           planBoundsCacheValid = false;
314   private Rectangle2D                       invalidPlanBounds;
315   private BufferedImage                     backgroundImageCache;
316   private Map<TextureImage, BufferedImage>  patternImagesCache;
317   private Set<HomePieceOfFurniture>         invalidFurnitureTopViewIcons;
318   private List<Wall>                        otherLevelsWallsCache;
319   private Area                              otherLevelsWallAreaCache;
320   private List<Room>                        otherLevelsRoomsCache;
321   private Area                              otherLevelsRoomAreaCache;
322   private Color                             wallsPatternBackgroundCache;
323   private Color                             wallsPatternForegroundCache;
324   private Map<Collection<Wall>, Area>       wallAreasCache;
325   private Map<HomeDoorOrWindow, Area>       doorOrWindowWallThicknessAreasCache;
326   private Map<HomeTexture, BufferedImage>   floorTextureImagesCache;
327   private Map<HomePieceOfFurniture, HomePieceOfFurnitureTopViewIconKey> furnitureTopViewIconKeys;
328   private Map<HomePieceOfFurnitureTopViewIconKey, PieceOfFurnitureTopViewIcon> furnitureTopViewIconsCache;
329 
330 
331   private static ExecutorService            backgroundImageLoader;
332 
333   private static final Shape       POINT_INDICATOR;
334   private static final GeneralPath FURNITURE_ROTATION_INDICATOR;
335   private static final GeneralPath FURNITURE_PITCH_ROTATION_INDICATOR;
336   private static final Shape       FURNITURE_ROLL_ROTATION_INDICATOR;
337   private static final GeneralPath FURNITURE_RESIZE_INDICATOR;
338   private static final GeneralPath ELEVATION_INDICATOR;
339   private static final Shape       ELEVATION_POINT_INDICATOR;
340   private static final GeneralPath FURNITURE_HEIGHT_INDICATOR;
341   private static final Shape       FURNITURE_HEIGHT_POINT_INDICATOR;
342   private static final GeneralPath LIGHT_POWER_INDICATOR;
343   private static final Shape       LIGHT_POWER_POINT_INDICATOR;
344   private static final GeneralPath WALL_ORIENTATION_INDICATOR;
345   private static final Shape       WALL_POINT;
346   private static final GeneralPath WALL_ARC_EXTENT_INDICATOR;
347   private static final GeneralPath WALL_AND_LINE_RESIZE_INDICATOR;
348   private static final Shape       CAMERA_YAW_ROTATION_INDICATOR;
349   private static final Shape       CAMERA_PITCH_ROTATION_INDICATOR;
350   private static final GeneralPath CAMERA_ELEVATION_INDICATOR;
351   private static final Shape       CAMERA_BODY;
352   private static final Shape       CAMERA_HEAD;
353   private static final GeneralPath DIMENSION_LINE_END;
354   private static final GeneralPath TEXT_LOCATION_INDICATOR;
355   private static final GeneralPath TEXT_ANGLE_INDICATOR;
356   private static final Shape       LABEL_CENTER_INDICATOR;
357   private static final Shape       COMPASS_DISC;
358   private static final GeneralPath COMPASS;
359   private static final GeneralPath COMPASS_ROTATION_INDICATOR;
360   private static final GeneralPath COMPASS_RESIZE_INDICATOR;
361 
362   private static final GeneralPath ARROW;
363 
364   private static final Stroke      INDICATOR_STROKE = new BasicStroke(1.5f);
365   private static final Stroke      POINT_STROKE = new BasicStroke(2f);
366 
367   private static final float       WALL_STROKE_WIDTH = 1.5f;
368   private static final float       BORDER_STROKE_WIDTH = 1f;
369   private static final float       ALIGNMENT_LINE_OFFSET = 25f;
370 
371   private static final BufferedImage ERROR_TEXTURE_IMAGE;
372   private static final BufferedImage WAIT_TEXTURE_IMAGE;
373 
374   static {
375     POINT_INDICATOR = new Ellipse2D.Float(-1.5f, -1.5f, 3, 3);
376 
377     // Create a path that draws an round arrow used as a rotation indicator
378     // at top left point of a piece of furniture
379     FURNITURE_ROTATION_INDICATOR = new GeneralPath();
FURNITURE_ROTATION_INDICATOR.append(POINT_INDICATOR, false)380     FURNITURE_ROTATION_INDICATOR.append(POINT_INDICATOR, false);
FURNITURE_ROTATION_INDICATOR.append(new Arc2D.Float(-8, -8, 16, 16, 45, 180, Arc2D.OPEN), false)381     FURNITURE_ROTATION_INDICATOR.append(new Arc2D.Float(-8, -8, 16, 16, 45, 180, Arc2D.OPEN), false);
382     FURNITURE_ROTATION_INDICATOR.moveTo(2.66f, -5.66f);
383     FURNITURE_ROTATION_INDICATOR.lineTo(5.66f, -5.66f);
384     FURNITURE_ROTATION_INDICATOR.lineTo(4f, -8.3f);
385 
386     // Create a path used as pitch rotation indicator
387     // at bottom left of a piece of furniture rotated around pitch
388     FURNITURE_PITCH_ROTATION_INDICATOR = new GeneralPath();
FURNITURE_PITCH_ROTATION_INDICATOR.append(POINT_INDICATOR, false)389     FURNITURE_PITCH_ROTATION_INDICATOR.append(POINT_INDICATOR, false);
390     FURNITURE_PITCH_ROTATION_INDICATOR.moveTo(-4.5f, 0);
391     FURNITURE_PITCH_ROTATION_INDICATOR.lineTo(-5.2f, 0);
392     FURNITURE_PITCH_ROTATION_INDICATOR.moveTo(-9f, 0);
393     FURNITURE_PITCH_ROTATION_INDICATOR.lineTo(-10, 0);
FURNITURE_PITCH_ROTATION_INDICATOR.append(new Arc2D.Float(-12, -8, 5, 16, 200, 320, Arc2D.OPEN), false)394     FURNITURE_PITCH_ROTATION_INDICATOR.append(new Arc2D.Float(-12, -8, 5, 16, 200, 320, Arc2D.OPEN), false);
395     FURNITURE_PITCH_ROTATION_INDICATOR.moveTo(-10f, -4.5f);
396     FURNITURE_PITCH_ROTATION_INDICATOR.lineTo(-12.3f, -2f);
397     FURNITURE_PITCH_ROTATION_INDICATOR.lineTo(-12.8f, -5.8f);
398 
399     // Create a path used as pitch rotation indicator
400     // at bottom left of a piece of furniture rotated around roll axis
401     AffineTransform transform = AffineTransform.getRotateInstance(-Math.PI / 2);
402     transform.concatenate(AffineTransform.getScaleInstance(1, -1));
403     FURNITURE_ROLL_ROTATION_INDICATOR = FURNITURE_PITCH_ROTATION_INDICATOR.createTransformedShape(transform);
404 
405     ELEVATION_POINT_INDICATOR = new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f);
406 
407     // Create a path that draws a line with one arrow as an elevation indicator
408     // at top right of a piece of furniture
409     ELEVATION_INDICATOR = new GeneralPath();
410     ELEVATION_INDICATOR.moveTo(0, -5); // Vertical line
411     ELEVATION_INDICATOR.lineTo(0, 5);
412     ELEVATION_INDICATOR.moveTo(-2.5f, 5);    // Bottom line
413     ELEVATION_INDICATOR.lineTo(2.5f, 5);
414     ELEVATION_INDICATOR.moveTo(-1.2f, 1.5f); // Bottom arrow
415     ELEVATION_INDICATOR.lineTo(0, 4.5f);
416     ELEVATION_INDICATOR.lineTo(1.2f, 1.5f);
417 
418     FURNITURE_HEIGHT_POINT_INDICATOR = new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f);
419 
420     // Create a path that draws a line with two arrows as a height indicator
421     // at bottom left of a piece of furniture
422     FURNITURE_HEIGHT_INDICATOR = new GeneralPath();
423     FURNITURE_HEIGHT_INDICATOR.moveTo(0, -6); // Vertical line
424     FURNITURE_HEIGHT_INDICATOR.lineTo(0, 6);
425     FURNITURE_HEIGHT_INDICATOR.moveTo(-2.5f, -6);    // Top line
426     FURNITURE_HEIGHT_INDICATOR.lineTo(2.5f, -6);
427     FURNITURE_HEIGHT_INDICATOR.moveTo(-2.5f, 6);     // Bottom line
428     FURNITURE_HEIGHT_INDICATOR.lineTo(2.5f, 6);
429     FURNITURE_HEIGHT_INDICATOR.moveTo(-1.2f, -2.5f); // Top arrow
430     FURNITURE_HEIGHT_INDICATOR.lineTo(0f, -5.5f);
431     FURNITURE_HEIGHT_INDICATOR.lineTo(1.2f, -2.5f);
432     FURNITURE_HEIGHT_INDICATOR.moveTo(-1.2f, 2.5f);  // Bottom arrow
433     FURNITURE_HEIGHT_INDICATOR.lineTo(0f, 5.5f);
434     FURNITURE_HEIGHT_INDICATOR.lineTo(1.2f, 2.5f);
435 
436     LIGHT_POWER_POINT_INDICATOR = new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f);
437 
438     // Create a path that draws a stripped triangle as a power indicator
439     // at bottom left of a not deformable lights
440     LIGHT_POWER_INDICATOR = new GeneralPath();
441     LIGHT_POWER_INDICATOR.moveTo(-8, 0);
442     LIGHT_POWER_INDICATOR.lineTo(-6f, 0);
443     LIGHT_POWER_INDICATOR.lineTo(-6f, -1);
LIGHT_POWER_INDICATOR.closePath()444     LIGHT_POWER_INDICATOR.closePath();
445     LIGHT_POWER_INDICATOR.moveTo(-3, 0);
446     LIGHT_POWER_INDICATOR.lineTo(-1f, 0);
447     LIGHT_POWER_INDICATOR.lineTo(-1f, -2.5f);
448     LIGHT_POWER_INDICATOR.lineTo(-3f, -1.8f);
LIGHT_POWER_INDICATOR.closePath()449     LIGHT_POWER_INDICATOR.closePath();
450     LIGHT_POWER_INDICATOR.moveTo(2, 0);
451     LIGHT_POWER_INDICATOR.lineTo(4, 0);
452     LIGHT_POWER_INDICATOR.lineTo(4f, -3.5f);
453     LIGHT_POWER_INDICATOR.lineTo(2f, -2.8f);
LIGHT_POWER_INDICATOR.closePath()454     LIGHT_POWER_INDICATOR.closePath();
455 
456     // Create a path used as a resize indicator
457     // at bottom right point of a piece of furniture
458     FURNITURE_RESIZE_INDICATOR = new GeneralPath();
FURNITURE_RESIZE_INDICATOR.append(new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f), false)459     FURNITURE_RESIZE_INDICATOR.append(new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f), false);
460     FURNITURE_RESIZE_INDICATOR.moveTo(5, -4);
461     FURNITURE_RESIZE_INDICATOR.lineTo(7, -4);
462     FURNITURE_RESIZE_INDICATOR.lineTo(7, 7);
463     FURNITURE_RESIZE_INDICATOR.lineTo(-4, 7);
464     FURNITURE_RESIZE_INDICATOR.lineTo(-4, 5);
465     FURNITURE_RESIZE_INDICATOR.moveTo(3.5f, 3.5f);
466     FURNITURE_RESIZE_INDICATOR.lineTo(9, 9);
467     FURNITURE_RESIZE_INDICATOR.moveTo(7, 9.5f);
468     FURNITURE_RESIZE_INDICATOR.lineTo(10, 10);
469     FURNITURE_RESIZE_INDICATOR.lineTo(9.5f, 7);
470 
471     // Create a path used an orientation indicator
472     // at start and end points of a selected wall
473     WALL_ORIENTATION_INDICATOR = new GeneralPath();
474     WALL_ORIENTATION_INDICATOR.moveTo(-4, -4);
475     WALL_ORIENTATION_INDICATOR.lineTo(4, 0);
476     WALL_ORIENTATION_INDICATOR.lineTo(-4, 4);
477 
478     WALL_POINT = new Ellipse2D.Float(-3, -3, 6, 6);
479 
480     // Create a path used as arc extent indicator for wall
481     WALL_ARC_EXTENT_INDICATOR = new GeneralPath();
WALL_ARC_EXTENT_INDICATOR.append(new Arc2D.Float(-4, 1, 8, 5, 210, 120, Arc2D.OPEN), false)482     WALL_ARC_EXTENT_INDICATOR.append(new Arc2D.Float(-4, 1, 8, 5, 210, 120, Arc2D.OPEN), false);
483     WALL_ARC_EXTENT_INDICATOR.moveTo(0, 6);
484     WALL_ARC_EXTENT_INDICATOR.lineTo(0, 11);
485     WALL_ARC_EXTENT_INDICATOR.moveTo(-1.8f, 8.7f);
486     WALL_ARC_EXTENT_INDICATOR.lineTo(0, 12);
487     WALL_ARC_EXTENT_INDICATOR.lineTo(1.8f, 8.7f);
488 
489     // Create a path used as a size indicator
490     // at start and end points of a selected wall
491     WALL_AND_LINE_RESIZE_INDICATOR = new GeneralPath();
492     WALL_AND_LINE_RESIZE_INDICATOR.moveTo(5, -2);
493     WALL_AND_LINE_RESIZE_INDICATOR.lineTo(5, 2);
494     WALL_AND_LINE_RESIZE_INDICATOR.moveTo(6, 0);
495     WALL_AND_LINE_RESIZE_INDICATOR.lineTo(11, 0);
496     WALL_AND_LINE_RESIZE_INDICATOR.moveTo(8.7f, -1.8f);
497     WALL_AND_LINE_RESIZE_INDICATOR.lineTo(12, 0);
498     WALL_AND_LINE_RESIZE_INDICATOR.lineTo(8.7f, 1.8f);
499 
500     // Create a path used as yaw rotation indicator for the camera
501     transform = AffineTransform.getRotateInstance(-Math.PI / 4);
502     CAMERA_YAW_ROTATION_INDICATOR = FURNITURE_ROTATION_INDICATOR.createTransformedShape(transform);
503 
504     // Create a path used as pitch rotation indicator for the camera
505     transform = AffineTransform.getRotateInstance(Math.PI);
506     CAMERA_PITCH_ROTATION_INDICATOR = FURNITURE_PITCH_ROTATION_INDICATOR.createTransformedShape(transform);
507 
508     // Create a path that draws a line with one arrow as an elevation indicator
509     // at the back of the camera
510     CAMERA_ELEVATION_INDICATOR = new GeneralPath();
511     CAMERA_ELEVATION_INDICATOR.moveTo(0, -4); // Vertical line
512     CAMERA_ELEVATION_INDICATOR.lineTo(0, 4);
513     CAMERA_ELEVATION_INDICATOR.moveTo(-2.5f, 4);    // Bottom line
514     CAMERA_ELEVATION_INDICATOR.lineTo(2.5f, 4);
515     CAMERA_ELEVATION_INDICATOR.moveTo(-1.2f, 0.5f); // Bottom arrow
516     CAMERA_ELEVATION_INDICATOR.lineTo(0, 3.5f);
517     CAMERA_ELEVATION_INDICATOR.lineTo(1.2f, 0.5f);
518 
519     // Create a path used to draw the camera
520     // This path looks like a human being seen from top that fits in one cm wide square
521     GeneralPath cameraBodyAreaPath = new GeneralPath();
cameraBodyAreaPath.append(new Ellipse2D.Float(-0.5f, -0.425f, 1f, 0.85f), false)522     cameraBodyAreaPath.append(new Ellipse2D.Float(-0.5f, -0.425f, 1f, 0.85f), false); // Body
cameraBodyAreaPath.append(new Ellipse2D.Float(-0.5f, -0.3f, 0.24f, 0.6f), false)523     cameraBodyAreaPath.append(new Ellipse2D.Float(-0.5f, -0.3f, 0.24f, 0.6f), false); // Shoulder
cameraBodyAreaPath.append(new Ellipse2D.Float(0.26f, -0.3f, 0.24f, 0.6f), false)524     cameraBodyAreaPath.append(new Ellipse2D.Float(0.26f, -0.3f, 0.24f, 0.6f), false); // Shoulder
525     CAMERA_BODY = new Area(cameraBodyAreaPath);
526 
527     GeneralPath cameraHeadAreaPath = new GeneralPath();
cameraHeadAreaPath.append(new Ellipse2D.Float(-0.18f, -0.45f, 0.36f, 1f), false)528     cameraHeadAreaPath.append(new Ellipse2D.Float(-0.18f, -0.45f, 0.36f, 1f), false); // Head
529     cameraHeadAreaPath.moveTo(-0.04f, 0.55f); // Noise
530     cameraHeadAreaPath.lineTo(0, 0.65f);
531     cameraHeadAreaPath.lineTo(0.04f, 0.55f);
cameraHeadAreaPath.closePath()532     cameraHeadAreaPath.closePath();
533     CAMERA_HEAD = new Area(cameraHeadAreaPath);
534 
535     DIMENSION_LINE_END = new GeneralPath();
536     DIMENSION_LINE_END.moveTo(-5, 5);
537     DIMENSION_LINE_END.lineTo(5, -5);
538     DIMENSION_LINE_END.moveTo(0, 5);
539     DIMENSION_LINE_END.lineTo(0, -5);
540 
541     // Create a path that draws three arrows going left, right and down
542     TEXT_LOCATION_INDICATOR = new GeneralPath();
TEXT_LOCATION_INDICATOR.append(new Arc2D.Float(-2, 0, 4, 4, 190, 160, Arc2D.CHORD), false)543     TEXT_LOCATION_INDICATOR.append(new Arc2D.Float(-2, 0, 4, 4, 190, 160, Arc2D.CHORD), false);
544     TEXT_LOCATION_INDICATOR.moveTo(0, 4);        // Down line
545     TEXT_LOCATION_INDICATOR.lineTo(0, 12);
546     TEXT_LOCATION_INDICATOR.moveTo(-1.2f, 8.5f); // Down arrow
547     TEXT_LOCATION_INDICATOR.lineTo(0f, 11.5f);
548     TEXT_LOCATION_INDICATOR.lineTo(1.2f, 8.5f);
549     TEXT_LOCATION_INDICATOR.moveTo(2f, 3f);      // Right line
550     TEXT_LOCATION_INDICATOR.lineTo(9, 6);
551     TEXT_LOCATION_INDICATOR.moveTo(6, 6.5f);     // Right arrow
552     TEXT_LOCATION_INDICATOR.lineTo(10, 7);
553     TEXT_LOCATION_INDICATOR.lineTo(7.5f, 3.5f);
554     TEXT_LOCATION_INDICATOR.moveTo(-2f, 3f);     // Left line
555     TEXT_LOCATION_INDICATOR.lineTo(-9, 6);
556     TEXT_LOCATION_INDICATOR.moveTo(-6, 6.5f);    // Left arrow
557     TEXT_LOCATION_INDICATOR.lineTo(-10, 7);
558     TEXT_LOCATION_INDICATOR.lineTo(-7.5f, 3.5f);
559 
560     // Create a path used as angle indicator for texts
561     TEXT_ANGLE_INDICATOR = new GeneralPath();
TEXT_ANGLE_INDICATOR.append(new Arc2D.Float(-1.25f, -1.25f, 2.5f, 2.5f, 10, 160, Arc2D.CHORD), false)562     TEXT_ANGLE_INDICATOR.append(new Arc2D.Float(-1.25f, -1.25f, 2.5f, 2.5f, 10, 160, Arc2D.CHORD), false);
TEXT_ANGLE_INDICATOR.append(new Arc2D.Float(-8, -8, 16, 16, 30, 120, Arc2D.OPEN), false)563     TEXT_ANGLE_INDICATOR.append(new Arc2D.Float(-8, -8, 16, 16, 30, 120, Arc2D.OPEN), false);
564     TEXT_ANGLE_INDICATOR.moveTo(4f, -5.2f);
565     TEXT_ANGLE_INDICATOR.lineTo(6.9f, -4f);
566     TEXT_ANGLE_INDICATOR.lineTo(5.8f, -7f);
567 
568     LABEL_CENTER_INDICATOR = new Ellipse2D.Float(-1f, -1f, 2, 2);
569 
570     // Create the path used to draw the compass
571     COMPASS_DISC = new Ellipse2D.Float(-0.5f, -0.5f, 1, 1);
572     BasicStroke stroke = new BasicStroke(0.01f);
573     COMPASS = new GeneralPath(stroke.createStrokedShape(COMPASS_DISC));
stroke.createStrokedShape(new Line2D.Float(-0.6f, 0, -0.5f, 0))574     COMPASS.append(stroke.createStrokedShape(new Line2D.Float(-0.6f, 0, -0.5f, 0)), false);
stroke.createStrokedShape(new Line2D.Float(0.6f, 0, 0.5f, 0))575     COMPASS.append(stroke.createStrokedShape(new Line2D.Float(0.6f, 0, 0.5f, 0)), false);
stroke.createStrokedShape(new Line2D.Float(0, 0.6f, 0, 0.5f))576     COMPASS.append(stroke.createStrokedShape(new Line2D.Float(0, 0.6f, 0, 0.5f)), false);
577     stroke = new BasicStroke(0.04f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
stroke.createStrokedShape(new Line2D.Float(0, 0, 0, 0))578     COMPASS.append(stroke.createStrokedShape(new Line2D.Float(0, 0, 0, 0)), false);
579     GeneralPath compassNeedle = new GeneralPath();
580     compassNeedle.moveTo(0, -0.47f);
581     compassNeedle.lineTo(0.15f, 0.46f);
582     compassNeedle.lineTo(0, 0.32f);
583     compassNeedle.lineTo(-0.15f, 0.46f);
compassNeedle.closePath()584     compassNeedle.closePath();
585     stroke = new BasicStroke(0.03f);
stroke.createStrokedShape(compassNeedle)586     COMPASS.append(stroke.createStrokedShape(compassNeedle), false);
587     GeneralPath compassNorthDirection = new GeneralPath();
588     compassNorthDirection.moveTo(-0.07f, -0.55f); // Draws the N letter
589     compassNorthDirection.lineTo(-0.07f, -0.69f);
590     compassNorthDirection.lineTo(0.07f, -0.56f);
591     compassNorthDirection.lineTo(0.07f, -0.7f);
stroke.createStrokedShape(compassNorthDirection)592     COMPASS.append(stroke.createStrokedShape(compassNorthDirection), false);
593 
594     // Create a path used as rotation indicator for the compass
595     COMPASS_ROTATION_INDICATOR = new GeneralPath();
COMPASS_ROTATION_INDICATOR.append(POINT_INDICATOR, false)596     COMPASS_ROTATION_INDICATOR.append(POINT_INDICATOR, false);
COMPASS_ROTATION_INDICATOR.append(new Arc2D.Float(-8, -7, 16, 16, 210, 120, Arc2D.OPEN), false)597     COMPASS_ROTATION_INDICATOR.append(new Arc2D.Float(-8, -7, 16, 16, 210, 120, Arc2D.OPEN), false);
598     COMPASS_ROTATION_INDICATOR.moveTo(4f, 5.66f);
599     COMPASS_ROTATION_INDICATOR.lineTo(7f, 5.66f);
600     COMPASS_ROTATION_INDICATOR.lineTo(5.6f, 8.3f);
601 
602     // Create a path used as a resize indicator for the compass
603     COMPASS_RESIZE_INDICATOR = new GeneralPath();
COMPASS_RESIZE_INDICATOR.append(new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f), false)604     COMPASS_RESIZE_INDICATOR.append(new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f), false);
605     COMPASS_RESIZE_INDICATOR.moveTo(4, -6);
606     COMPASS_RESIZE_INDICATOR.lineTo(6, -6);
607     COMPASS_RESIZE_INDICATOR.lineTo(6, 6);
608     COMPASS_RESIZE_INDICATOR.lineTo(4, 6);
609     COMPASS_RESIZE_INDICATOR.moveTo(5, 0);
610     COMPASS_RESIZE_INDICATOR.lineTo(9, 0);
611     COMPASS_RESIZE_INDICATOR.moveTo(9, -1.5f);
612     COMPASS_RESIZE_INDICATOR.lineTo(12, 0);
613     COMPASS_RESIZE_INDICATOR.lineTo(9, 1.5f);
614 
615     ARROW = new GeneralPath();
616     ARROW.moveTo(-5, -2);
617     ARROW.lineTo(0, 0);
618     ARROW.lineTo(-5, 2);
619 
620     ERROR_TEXTURE_IMAGE = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
621     Graphics g = ERROR_TEXTURE_IMAGE.getGraphics();
622     g.setColor(Color.RED);
623     g.drawLine(0, 0, 0, 0);
g.dispose()624     g.dispose();
625 
626     WAIT_TEXTURE_IMAGE = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
627     g = WAIT_TEXTURE_IMAGE.getGraphics();
628     g.setColor(Color.WHITE);
629     g.drawLine(0, 0, 0, 0);
g.dispose()630     g.dispose();
631   }
632 
633   /**
634    * Creates a new plan that displays <code>home</code>.
635    * @param home the home to display
636    * @param preferences user preferences to retrieve used unit, grid visibility...
637    * @param controller the optional controller used to manage home items modification
638    */
PlanComponent(Home home, UserPreferences preferences, PlanController controller)639   public PlanComponent(Home home,
640                        UserPreferences preferences,
641                        PlanController controller) {
642     this(home, preferences, null, controller);
643   }
644 
645   /**
646    * Creates a new plan that displays <code>home</code>.
647    * @param home the home to display
648    * @param preferences user preferences to retrieve used unit, grid visibility...
649    * @param object3dFactory a factory able to create 3D objects from <code>home</code> furniture.
650    *            The {@link Object3DFactory#createObject3D(Home, Selectable, boolean) createObject3D} of
651    *            this factory is expected to return an instance of {@link Object3DBranch} in current implementation.
652    * @param controller the optional controller used to manage home items modification
653    */
PlanComponent(Home home, UserPreferences preferences, Object3DFactory object3dFactory, PlanController controller)654   public PlanComponent(Home home,
655                        UserPreferences preferences,
656                        Object3DFactory  object3dFactory,
657                        PlanController controller) {
658     this.home = home;
659     this.preferences = preferences;
660     try {
661       if (object3dFactory == null && !Boolean.getBoolean("com.eteks.sweethome3d.no3D")) {
662         object3dFactory = new Object3DBranchFactory();
663       }
664     } catch (AccessControlException ex) {
665       // Can't access to properties
666     }
667     this.object3dFactory = object3dFactory;
668     // Set JComponent default properties
669     setOpaque(true);
670     // Add listeners
671     addModelListeners(home, preferences, controller);
672     createToolTipTextFields(preferences, controller);
673     if (controller != null) {
674       addMouseListeners(controller);
675       addFocusListener(controller);
676       addControllerListener(controller);
677       createActions(controller);
678       installDefaultKeyboardActions();
679       setFocusable(true);
680       setAutoscrolls(true);
681     }
682     this.rotationCursor = createCustomCursor("resources/cursors/rotation16x16.png",
683         "resources/cursors/rotation32x32.png", "Rotation cursor", Cursor.MOVE_CURSOR);
684     this.elevationCursor = createCustomCursor("resources/cursors/elevation16x16.png",
685         "resources/cursors/elevation32x32.png", "Elevation cursor", Cursor.MOVE_CURSOR);
686     this.heightCursor = createCustomCursor("resources/cursors/height16x16.png",
687         "resources/cursors/height32x32.png", "Height cursor", Cursor.MOVE_CURSOR);
688     this.powerCursor = createCustomCursor("resources/cursors/power16x16.png",
689         "resources/cursors/power32x32.png", "Power cursor", Cursor.MOVE_CURSOR);
690     this.resizeCursor = createCustomCursor("resources/cursors/resize16x16.png",
691         "resources/cursors/resize32x32.png", "Resize cursor", Cursor.MOVE_CURSOR);
692     this.moveCursor = createCustomCursor("resources/cursors/move16x16.png",
693         "resources/cursors/move32x32.png", "Move cursor", Cursor.MOVE_CURSOR);
694     this.panningCursor = createCustomCursor("resources/cursors/panning16x16.png",
695         "resources/cursors/panning32x32.png", "Panning cursor", Cursor.HAND_CURSOR);
696     this.duplicationCursor = DragSource.DefaultCopyDrop;
697     this.patternImagesCache = new HashMap<TextureImage, BufferedImage>();
698     // Install default colors using same colors as a text field
699     super.setForeground(UIManager.getColor("TextField.foreground"));
700     super.setBackground(UIManager.getColor("TextField.background"));
701   }
702 
703   /**
704    * Adds home items and selection listeners on this component to receive
705    * changes notifications from home.
706    */
addModelListeners(final Home home, final UserPreferences preferences, final PlanController controller)707   private void addModelListeners(final Home home, final UserPreferences preferences,
708                                  final PlanController controller) {
709     // Add listener to update plan when furniture changes
710     final PropertyChangeListener furnitureChangeListener = new PropertyChangeListener() {
711         public void propertyChange(final PropertyChangeEvent ev) {
712           if (furnitureTopViewIconKeys != null
713               && (HomePieceOfFurniture.Property.MODEL.name().equals(ev.getPropertyName())
714                   || HomePieceOfFurniture.Property.MODEL_ROTATION.name().equals(ev.getPropertyName())
715                   || HomePieceOfFurniture.Property.BACK_FACE_SHOWN.name().equals(ev.getPropertyName())
716                   || HomePieceOfFurniture.Property.MODEL_TRANSFORMATIONS.name().equals(ev.getPropertyName())
717                   || HomePieceOfFurniture.Property.ROLL.name().equals(ev.getPropertyName())
718                   || HomePieceOfFurniture.Property.PITCH.name().equals(ev.getPropertyName())
719                   || (HomePieceOfFurniture.Property.WIDTH_IN_PLAN.name().equals(ev.getPropertyName())
720                       || HomePieceOfFurniture.Property.DEPTH_IN_PLAN.name().equals(ev.getPropertyName())
721                       || HomePieceOfFurniture.Property.HEIGHT_IN_PLAN.name().equals(ev.getPropertyName()))
722                      && (((HomePieceOfFurniture)ev.getSource()).isHorizontallyRotated()
723                          || ((HomePieceOfFurniture)ev.getSource()).getTexture() != null)
724                   || HomePieceOfFurniture.Property.MODEL_MIRRORED.name().equals(ev.getPropertyName())
725                      && ((HomePieceOfFurniture)ev.getSource()).getRoll() != 0)) {
726             if (HomePieceOfFurniture.Property.HEIGHT_IN_PLAN.name().equals(ev.getPropertyName())) {
727               sortedLevelFurniture = null;
728             }
729             if (!(ev.getSource() instanceof HomeFurnitureGroup)) {
730               // Invalidate top icon only for individual pieces because
731               // groups can't have their own texture, can't be transformed and can't be horizontally rotated
732               if (controller == null || !controller.isModificationState()) {
733                 furnitureTopViewIconKeys.remove((HomePieceOfFurniture)ev.getSource());
734               } else {
735                 // Delay computing of new top view icon
736                 if (invalidFurnitureTopViewIcons == null) {
737                   invalidFurnitureTopViewIcons = new HashSet<HomePieceOfFurniture>();
738                   controller.addPropertyChangeListener(PlanController.Property.MODIFICATION_STATE, new PropertyChangeListener() {
739                       public void propertyChange(PropertyChangeEvent ev2) {
740                         for (HomePieceOfFurniture piece : invalidFurnitureTopViewIcons) {
741                           furnitureTopViewIconKeys.remove(piece);
742                         }
743                         invalidFurnitureTopViewIcons = null;
744                         repaint();
745                         controller.removePropertyChangeListener(PlanController.Property.MODIFICATION_STATE, this);
746                       }
747                     });
748                 }
749                 invalidFurnitureTopViewIcons.add((HomePieceOfFurniture)ev.getSource());
750               }
751             }
752             revalidate();
753           } else if (furnitureTopViewIconKeys != null
754                       && (HomePieceOfFurniture.Property.PLAN_ICON.name().equals(ev.getPropertyName())
755                           || HomePieceOfFurniture.Property.COLOR.name().equals(ev.getPropertyName())
756                           || HomePieceOfFurniture.Property.TEXTURE.name().equals(ev.getPropertyName())
757                           || HomePieceOfFurniture.Property.MODEL_MATERIALS.name().equals(ev.getPropertyName())
758                           || HomePieceOfFurniture.Property.SHININESS.name().equals(ev.getPropertyName()))) {
759             // From version 5.2, these changes can happen only for individual pieces because groups
760             // can't have their own color, texture, materials and shininess anymore
761             furnitureTopViewIconKeys.remove((HomePieceOfFurniture)ev.getSource());
762             repaint();
763           } else if (HomePieceOfFurniture.Property.ELEVATION.name().equals(ev.getPropertyName())
764                      || HomePieceOfFurniture.Property.LEVEL.name().equals(ev.getPropertyName())
765                      || HomePieceOfFurniture.Property.HEIGHT_IN_PLAN.name().equals(ev.getPropertyName())) {
766             sortedLevelFurniture = null;
767             repaint();
768           } else if (HomePieceOfFurniture.Property.ICON.name().equals(ev.getPropertyName())
769                      || HomeDoorOrWindow.Property.WALL_CUT_OUT_ON_BOTH_SIDES.name().equals(ev.getPropertyName())) {
770             // Should repaint only if icons rather than plan icons or top views are drawn but this may depends on various criteria
771             repaint();
772           } else if (doorOrWindowWallThicknessAreasCache != null
773                      && (HomePieceOfFurniture.Property.WIDTH.name().equals(ev.getPropertyName())
774                          || HomePieceOfFurniture.Property.DEPTH.name().equals(ev.getPropertyName())
775                          || HomePieceOfFurniture.Property.ANGLE.name().equals(ev.getPropertyName())
776                          || HomePieceOfFurniture.Property.MODEL_MIRRORED.name().equals(ev.getPropertyName())
777                          || HomePieceOfFurniture.Property.X.name().equals(ev.getPropertyName())
778                          || HomePieceOfFurniture.Property.Y.name().equals(ev.getPropertyName())
779                          || HomePieceOfFurniture.Property.LEVEL.name().equals(ev.getPropertyName())
780                          || HomeDoorOrWindow.Property.WALL_THICKNESS.name().equals(ev.getPropertyName())
781                          || HomeDoorOrWindow.Property.WALL_DISTANCE.name().equals(ev.getPropertyName())
782                          || HomeDoorOrWindow.Property.WALL_WIDTH.name().equals(ev.getPropertyName())
783                          || HomeDoorOrWindow.Property.WALL_LEFT.name().equals(ev.getPropertyName())
784                          || HomeDoorOrWindow.Property.CUT_OUT_SHAPE.name().equals(ev.getPropertyName()))
785                      && doorOrWindowWallThicknessAreasCache.remove(ev.getSource()) != null) {
786             revalidate();
787           } else {
788             revalidate();
789           }
790         }
791       };
792     for (HomePieceOfFurniture piece : home.getFurniture()) {
793       piece.addPropertyChangeListener(furnitureChangeListener);
794       if (piece instanceof HomeFurnitureGroup) {
795         for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
796           childPiece.addPropertyChangeListener(furnitureChangeListener);
797         }
798       }
799     }
800     home.addFurnitureListener(new CollectionListener<HomePieceOfFurniture>() {
801         public void collectionChanged(CollectionEvent<HomePieceOfFurniture> ev) {
802           HomePieceOfFurniture piece = ev.getItem();
803           if (ev.getType() == CollectionEvent.Type.ADD) {
804             piece.addPropertyChangeListener(furnitureChangeListener);
805             if (piece instanceof HomeFurnitureGroup) {
806               for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
807                 childPiece.addPropertyChangeListener(furnitureChangeListener);
808               }
809             }
810           } else if (ev.getType() == CollectionEvent.Type.DELETE) {
811             piece.removePropertyChangeListener(furnitureChangeListener);
812             if (piece instanceof HomeFurnitureGroup) {
813               for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
814                 childPiece.removePropertyChangeListener(furnitureChangeListener);
815               }
816             }
817           }
818           sortedLevelFurniture = null;
819           revalidate();
820         }
821       });
822 
823     // Add listener to update plan when walls change
824     final PropertyChangeListener wallChangeListener = new PropertyChangeListener() {
825         public void propertyChange(PropertyChangeEvent ev) {
826           String propertyName = ev.getPropertyName();
827           if (Wall.Property.X_START.name().equals(propertyName)
828               || Wall.Property.X_END.name().equals(propertyName)
829               || Wall.Property.Y_START.name().equals(propertyName)
830               || Wall.Property.Y_END.name().equals(propertyName)
831               || Wall.Property.WALL_AT_START.name().equals(propertyName)
832               || Wall.Property.WALL_AT_END.name().equals(propertyName)
833               || Wall.Property.THICKNESS.name().equals(propertyName)
834               || Wall.Property.ARC_EXTENT.name().equals(propertyName)
835               || Wall.Property.PATTERN.name().equals(propertyName)) {
836             if (home.isAllLevelsSelection()) {
837               otherLevelsWallAreaCache = null;
838               otherLevelsWallsCache = null;
839             }
840             wallAreasCache = null;
841             doorOrWindowWallThicknessAreasCache = null;
842             revalidate();
843           } else if (Wall.Property.LEVEL.name().equals(propertyName)
844               || Wall.Property.HEIGHT.name().equals(propertyName)
845               || Wall.Property.HEIGHT_AT_END.name().equals(propertyName)) {
846             otherLevelsWallAreaCache = null;
847             otherLevelsWallsCache = null;
848             wallAreasCache = null;
849             repaint();
850           }
851         }
852       };
853     for (Wall wall : home.getWalls()) {
854       wall.addPropertyChangeListener(wallChangeListener);
855     }
856     home.addWallsListener(new CollectionListener<Wall> () {
857         public void collectionChanged(CollectionEvent<Wall> ev) {
858           if (ev.getType() == CollectionEvent.Type.ADD) {
859             ev.getItem().addPropertyChangeListener(wallChangeListener);
860           } else if (ev.getType() == CollectionEvent.Type.DELETE) {
861             ev.getItem().removePropertyChangeListener(wallChangeListener);
862           }
863           otherLevelsWallAreaCache = null;
864           otherLevelsWallsCache = null;
865           wallAreasCache = null;
866           doorOrWindowWallThicknessAreasCache = null;
867           revalidate();
868         }
869       });
870 
871     // Add listener to update plan when rooms change
872     final PropertyChangeListener roomChangeListener = new PropertyChangeListener() {
873         public void propertyChange(PropertyChangeEvent ev) {
874           String propertyName = ev.getPropertyName();
875           if (Room.Property.POINTS.name().equals(propertyName)
876               || Room.Property.NAME.name().equals(propertyName)
877               || Room.Property.NAME_X_OFFSET.name().equals(propertyName)
878               || Room.Property.NAME_Y_OFFSET.name().equals(propertyName)
879               || Room.Property.NAME_STYLE.name().equals(propertyName)
880               || Room.Property.NAME_ANGLE.name().equals(propertyName)
881               || Room.Property.AREA_VISIBLE.name().equals(propertyName)
882               || Room.Property.AREA_X_OFFSET.name().equals(propertyName)
883               || Room.Property.AREA_Y_OFFSET.name().equals(propertyName)
884               || Room.Property.AREA_STYLE.name().equals(propertyName)
885               || Room.Property.AREA_ANGLE.name().equals(propertyName)) {
886             sortedLevelRooms = null;
887             otherLevelsRoomAreaCache = null;
888             otherLevelsRoomsCache = null;
889             revalidate();
890           } else if (preferences.isRoomFloorColoredOrTextured()
891                      && (Room.Property.FLOOR_COLOR.name().equals(propertyName)
892                          || Room.Property.FLOOR_TEXTURE.name().equals(propertyName)
893                          || Room.Property.FLOOR_VISIBLE.name().equals(propertyName))) {
894             repaint();
895           }
896         }
897       };
898     for (Room room : home.getRooms()) {
899       room.addPropertyChangeListener(roomChangeListener);
900     }
901     home.addRoomsListener(new CollectionListener<Room> () {
902         public void collectionChanged(CollectionEvent<Room> ev) {
903           if (ev.getType() == CollectionEvent.Type.ADD) {
904             ev.getItem().addPropertyChangeListener(roomChangeListener);
905           } else if (ev.getType() == CollectionEvent.Type.DELETE) {
906             ev.getItem().removePropertyChangeListener(roomChangeListener);
907           }
908           sortedLevelRooms = null;
909           otherLevelsRoomAreaCache = null;
910           otherLevelsRoomsCache = null;
911           revalidate();
912         }
913       });
914 
915      // Add listener to update plan when polylines change
916      final PropertyChangeListener changeListener = new PropertyChangeListener() {
917          public void propertyChange(PropertyChangeEvent ev) {
918            String propertyName = ev.getPropertyName();
919            if (Polyline.Property.COLOR.name().equals(propertyName)
920                || Polyline.Property.DASH_STYLE.name().equals(propertyName)) {
921              repaint();
922            } else {
923              revalidate();
924            }
925          }
926        };
927      for (Polyline polyline : home.getPolylines()) {
928        polyline.addPropertyChangeListener(changeListener);
929      }
930      home.addPolylinesListener(new CollectionListener<Polyline>() {
931         public void collectionChanged(CollectionEvent<Polyline> ev) {
932           if (ev.getType() == CollectionEvent.Type.ADD) {
933             ev.getItem().addPropertyChangeListener(changeListener);
934           } else if (ev.getType() == CollectionEvent.Type.DELETE) {
935             ev.getItem().removePropertyChangeListener(changeListener);
936           }
937           revalidate();
938         }
939       });
940 
941     // Add listener to update plan when dimension lines change
942     final PropertyChangeListener dimensionLineChangeListener = new PropertyChangeListener() {
943         public void propertyChange(PropertyChangeEvent ev) {
944           revalidate();
945         }
946       };
947     for (DimensionLine dimensionLine : home.getDimensionLines()) {
948       dimensionLine.addPropertyChangeListener(dimensionLineChangeListener);
949     }
950     home.addDimensionLinesListener(new CollectionListener<DimensionLine> () {
951         public void collectionChanged(CollectionEvent<DimensionLine> ev) {
952           if (ev.getType() == CollectionEvent.Type.ADD) {
953             ev.getItem().addPropertyChangeListener(dimensionLineChangeListener);
954           } else if (ev.getType() == CollectionEvent.Type.DELETE) {
955             ev.getItem().removePropertyChangeListener(dimensionLineChangeListener);
956           }
957           revalidate();
958         }
959       });
960 
961     // Add listener to update plan when labels change
962     final PropertyChangeListener labelChangeListener = new PropertyChangeListener() {
963         public void propertyChange(PropertyChangeEvent ev) {
964           revalidate();
965         }
966       };
967     for (Label label : home.getLabels()) {
968       label.addPropertyChangeListener(labelChangeListener);
969     }
970     home.addLabelsListener(new CollectionListener<Label> () {
971         public void collectionChanged(CollectionEvent<Label> ev) {
972           if (ev.getType() == CollectionEvent.Type.ADD) {
973             ev.getItem().addPropertyChangeListener(labelChangeListener);
974           } else if (ev.getType() == CollectionEvent.Type.DELETE) {
975             ev.getItem().removePropertyChangeListener(labelChangeListener);
976           }
977           revalidate();
978         }
979       });
980 
981     // Add listener to update plan when levels change
982     final PropertyChangeListener levelChangeListener = new PropertyChangeListener() {
983         public void propertyChange(PropertyChangeEvent ev) {
984           String propertyName = ev.getPropertyName();
985           if (Level.Property.BACKGROUND_IMAGE.name().equals(propertyName)) {
986             backgroundImageCache = null;
987             revalidate();
988           } else if (Level.Property.ELEVATION.name().equals(propertyName)
989                      || Level.Property.ELEVATION_INDEX.name().equals(propertyName)
990                      || Level.Property.VIEWABLE.name().equals(propertyName)) {
991             backgroundImageCache = null;
992             otherLevelsWallAreaCache = null;
993             otherLevelsWallsCache = null;
994             otherLevelsRoomAreaCache = null;
995             otherLevelsRoomsCache = null;
996             wallAreasCache = null;
997             doorOrWindowWallThicknessAreasCache = null;
998             sortedLevelFurniture = null;
999             sortedLevelRooms = null;
1000             repaint();
1001           }
1002         }
1003       };
1004     for (Level level : home.getLevels()) {
1005       level.addPropertyChangeListener(levelChangeListener);
1006     }
1007     home.addLevelsListener(new CollectionListener<Level> () {
1008         public void collectionChanged(CollectionEvent<Level> ev) {
1009           Level level = ev.getItem();
1010           if (ev.getType() == CollectionEvent.Type.ADD) {
1011             level.addPropertyChangeListener(levelChangeListener);
1012           } else if (ev.getType() == CollectionEvent.Type.DELETE) {
1013             level.removePropertyChangeListener(levelChangeListener);
1014           }
1015           revalidate();
1016         }
1017       });
1018 
1019     home.addPropertyChangeListener(Home.Property.CAMERA, new PropertyChangeListener() {
1020         public void propertyChange(PropertyChangeEvent ev) {
1021           revalidate();
1022         }
1023       });
1024     home.getObserverCamera().addPropertyChangeListener(new PropertyChangeListener() {
1025         public void propertyChange(PropertyChangeEvent ev) {
1026           String propertyName = ev.getPropertyName();
1027           if (Camera.Property.X.name().equals(propertyName)
1028               || Camera.Property.Y.name().equals(propertyName)
1029               || Camera.Property.FIELD_OF_VIEW.name().equals(propertyName)
1030               || Camera.Property.YAW.name().equals(propertyName)
1031               || ObserverCamera.Property.WIDTH.name().equals(propertyName)
1032               || ObserverCamera.Property.DEPTH.name().equals(propertyName)
1033               || ObserverCamera.Property.HEIGHT.name().equals(propertyName)) {
1034             revalidate();
1035           }
1036         }
1037       });
1038     home.getCompass().addPropertyChangeListener(new PropertyChangeListener() {
1039         public void propertyChange(PropertyChangeEvent ev) {
1040           String propertyName = ev.getPropertyName();
1041           if (Compass.Property.X.name().equals(propertyName)
1042               || Compass.Property.Y.name().equals(propertyName)
1043               || Compass.Property.NORTH_DIRECTION.name().equals(propertyName)
1044               || Compass.Property.DIAMETER.name().equals(propertyName)
1045               || Compass.Property.VISIBLE.name().equals(propertyName)) {
1046             revalidate();
1047           }
1048         }
1049       });
1050     home.addSelectionListener(new SelectionListener () {
1051         public void selectionChanged(SelectionEvent ev) {
1052           repaint();
1053         }
1054       });
1055     home.addPropertyChangeListener(Home.Property.BACKGROUND_IMAGE,
1056       new PropertyChangeListener() {
1057         public void propertyChange(PropertyChangeEvent ev) {
1058           backgroundImageCache = null;
1059           repaint();
1060         }
1061       });
1062     home.addPropertyChangeListener(Home.Property.SELECTED_LEVEL, new PropertyChangeListener() {
1063         public void propertyChange(PropertyChangeEvent ev) {
1064           backgroundImageCache = null;
1065           otherLevelsWallAreaCache = null;
1066           otherLevelsWallsCache = null;
1067           otherLevelsRoomAreaCache = null;
1068           otherLevelsRoomsCache = null;
1069           wallAreasCache = null;
1070           doorOrWindowWallThicknessAreasCache = null;
1071           sortedLevelRooms = null;
1072           sortedLevelFurniture = null;
1073           repaint();
1074         }
1075       });
1076     UserPreferencesChangeListener preferencesListener = new UserPreferencesChangeListener(this);
1077     preferences.addPropertyChangeListener(UserPreferences.Property.UNIT, preferencesListener);
1078     preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE, preferencesListener);
1079     preferences.addPropertyChangeListener(UserPreferences.Property.GRID_VISIBLE, preferencesListener);
1080     preferences.addPropertyChangeListener(UserPreferences.Property.DEFAULT_FONT_NAME, preferencesListener);
1081     preferences.addPropertyChangeListener(UserPreferences.Property.FURNITURE_VIEWED_FROM_TOP, preferencesListener);
1082     preferences.addPropertyChangeListener(UserPreferences.Property.FURNITURE_MODEL_ICON_SIZE, preferencesListener);
1083     preferences.addPropertyChangeListener(UserPreferences.Property.ROOM_FLOOR_COLORED_OR_TEXTURED, preferencesListener);
1084     preferences.addPropertyChangeListener(UserPreferences.Property.WALL_PATTERN, preferencesListener);
1085   }
1086 
1087   /**
1088    * Preferences property listener bound to this component with a weak reference to avoid
1089    * strong link between preferences and this component.
1090    */
1091   private static class UserPreferencesChangeListener implements PropertyChangeListener {
1092     private WeakReference<PlanComponent>  planComponent;
1093 
UserPreferencesChangeListener(PlanComponent planComponent)1094     public UserPreferencesChangeListener(PlanComponent planComponent) {
1095       this.planComponent = new WeakReference<PlanComponent>(planComponent);
1096     }
1097 
propertyChange(PropertyChangeEvent ev)1098     public void propertyChange(PropertyChangeEvent ev) {
1099       // If plan component was garbage collected, remove this listener from preferences
1100       PlanComponent planComponent = this.planComponent.get();
1101       UserPreferences preferences = (UserPreferences)ev.getSource();
1102       UserPreferences.Property property = UserPreferences.Property.valueOf(ev.getPropertyName());
1103       if (planComponent == null) {
1104         preferences.removePropertyChangeListener(property, this);
1105       } else {
1106         switch (property) {
1107           case LANGUAGE :
1108           case UNIT :
1109             // Update format of tool tip text fields
1110             for (Map.Entry<PlanController.EditableProperty, JFormattedTextField> toolTipTextFieldEntry :
1111               planComponent.toolTipEditableTextFields.entrySet()) {
1112               updateToolTipTextFieldFormatterFactory(toolTipTextFieldEntry.getValue(),
1113                   toolTipTextFieldEntry.getKey(), preferences);
1114             }
1115             if (planComponent.horizontalRuler != null) {
1116               planComponent.horizontalRuler.repaint();
1117             }
1118             if (planComponent.verticalRuler != null) {
1119               planComponent.verticalRuler.repaint();
1120             }
1121             break;
1122           case DEFAULT_FONT_NAME :
1123             planComponent.fonts = null;
1124             planComponent.fontsMetrics = null;
1125             planComponent.revalidate();
1126             break;
1127           case WALL_PATTERN :
1128             planComponent.wallAreasCache = null;
1129             break;
1130           case FURNITURE_VIEWED_FROM_TOP :
1131             if (planComponent.furnitureTopViewIconKeys != null
1132                 && !preferences.isFurnitureViewedFromTop()) {
1133               planComponent.furnitureTopViewIconKeys = null;
1134               planComponent.furnitureTopViewIconsCache = null;
1135             }
1136             break;
1137           case FURNITURE_MODEL_ICON_SIZE :
1138             planComponent.furnitureTopViewIconKeys = null;
1139             planComponent.furnitureTopViewIconsCache = null;
1140             break;
1141           default:
1142             break;
1143         }
1144         planComponent.repaint();
1145       }
1146     }
1147   }
1148 
1149   /**
1150    * Revalidates and repaints this component and its rulers.
1151    */
1152   @Override
revalidate()1153   public void revalidate() {
1154     // Revalidate and repaint
1155     super.revalidate();
1156     repaint();
1157 
1158     if (this.horizontalRuler != null) {
1159       this.horizontalRuler.revalidate();
1160       this.horizontalRuler.repaint();
1161     }
1162     if (this.verticalRuler != null) {
1163       this.verticalRuler.revalidate();
1164       this.verticalRuler.repaint();
1165     }
1166   }
1167 
1168   /**
1169    * Invalidates this component voiding plan bounds cache if <code>invalidatePlanBoundsCache</code> is <code>true</code>.
1170    */
invalidate(boolean invalidatePlanBoundsCache)1171   private void invalidate(boolean invalidatePlanBoundsCache) {
1172     if (isValid()) {
1173       if (invalidatePlanBoundsCache) {
1174         boolean planBoundsCacheWereValid = this.planBoundsCacheValid;
1175         if (this.invalidPlanBounds == null) {
1176           this.invalidPlanBounds = getPlanBounds().getBounds2D();
1177         }
1178         if (planBoundsCacheWereValid) {
1179           this.planBoundsCacheValid = false;
1180         }
1181       }
1182       super.invalidate();
1183     }
1184   }
1185 
1186   @Override
invalidate()1187   public void invalidate() {
1188     invalidate(true);
1189   }
1190 
1191   /**
1192    * Validates this component and updates viewport position if it's displayed in a scrolled pane.
1193    */
1194   @Override
validate()1195   public void validate() {
1196     super.validate();
1197     if (this.invalidPlanBounds != null
1198         && getParent() instanceof JViewport) {
1199       float planBoundsNewMinX = (float)getPlanBounds().getMinX();
1200       float planBoundsNewMinY = (float)getPlanBounds().getMinY();
1201       // If plan bounds upper left corner diminished
1202       if (planBoundsNewMinX < this.invalidPlanBounds.getMinX()
1203           || planBoundsNewMinY < this.invalidPlanBounds.getMinY()) {
1204         JViewport parent = (JViewport)getParent();
1205         final Point viewPosition = parent.getViewPosition();
1206         Dimension extentSize = parent.getExtentSize();
1207         Dimension viewSize = parent.getViewSize();
1208         // Update view position when scroll bars are visible
1209         if (extentSize.width < viewSize.width
1210             || extentSize.height < viewSize.height) {
1211           int deltaX = convertLengthToPixel(this.invalidPlanBounds.getMinX() - planBoundsNewMinX);
1212           int deltaY = convertLengthToPixel(this.invalidPlanBounds.getMinY() - planBoundsNewMinY);
1213           parent.setViewPosition(new Point(viewPosition.x + deltaX, viewPosition.y + deltaY));
1214         }
1215       }
1216     }
1217     this.invalidPlanBounds = null;
1218   }
1219 
1220   /**
1221    * Adds AWT mouse listeners to this component that calls back <code>controller</code> methods.
1222    */
addMouseListeners(final PlanController controller)1223   private void addMouseListeners(final PlanController controller) {
1224     MouseInputAdapter mouseListener = new MouseInputAdapter() {
1225         private Point lastMousePressedLocation;
1226 
1227         @Override
1228         public void mousePressed(MouseEvent ev) {
1229           this.lastMousePressedLocation = ev.getPoint();
1230           if (isEnabled() && !ev.isPopupTrigger()) {
1231             requestFocusInWindow();
1232             if (SwingUtilities.isLeftMouseButton(ev)) {
1233               boolean alignmentActivated = OperatingSystem.isWindows() || OperatingSystem.isMacOSX()
1234                   ? ev.isShiftDown()
1235                   : ev.isShiftDown() && !ev.isAltDown();
1236               boolean duplicationActivated = OperatingSystem.isMacOSX()
1237                   ? ev.isAltDown()
1238                   : ev.isControlDown();
1239               boolean magnetismToggled = OperatingSystem.isWindows()
1240                   ? ev.isAltDown()
1241                   : (OperatingSystem.isMacOSX()
1242                          ? ev.isMetaDown()
1243                          : ev.isShiftDown() && ev.isAltDown());
1244               controller.pressMouse(convertXPixelToModel(ev.getX()), convertYPixelToModel(ev.getY()),
1245                   ev.getClickCount(), ev.isShiftDown() && !ev.isControlDown() && !ev.isAltDown() && !ev.isMetaDown(),
1246                   alignmentActivated, duplicationActivated, magnetismToggled);
1247 
1248               if (OperatingSystem.isWindows()) {
1249                 // While mouse is pressed, prevent Alt released event from transferring focus to menu bar and toggling magnetism
1250                 // See https://stackoverflow.com/questions/56339708/disable-single-alt-type-to-activate-the-menu
1251                 KeyboardFocusManager currentManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
1252                 try {
1253                   Method method = KeyboardFocusManager.class.getDeclaredMethod("getKeyEventPostProcessors");
1254                   method.setAccessible(true);
1255                   @SuppressWarnings("unchecked")
1256                   List<KeyEventPostProcessor> processors = (List<KeyEventPostProcessor>)method.invoke(currentManager);
1257                   for (KeyEventPostProcessor processor : processors) {
1258                     if ("AltProcessor".equals(processor.getClass().getSimpleName())) {
1259                       windowsAltPostProcessor = processor;
1260                       currentManager.removeKeyEventPostProcessor(windowsAltPostProcessor);
1261                       break;
1262                     }
1263                   }
1264                 } catch (Exception ex) {
1265                   ex.printStackTrace();
1266                 }
1267               }
1268             }
1269           }
1270         }
1271 
1272         @Override
1273         public void mouseMoved(MouseEvent ev) {
1274           // Ignore mouseMoved events that follows a mousePressed at the same location (Linux notifies this kind of events)
1275           if (this.lastMousePressedLocation != null
1276               && !this.lastMousePressedLocation.equals(ev.getPoint())) {
1277             this.lastMousePressedLocation = null;
1278           }
1279           if (this.lastMousePressedLocation == null) {
1280             if (isEnabled()) {
1281               controller.moveMouse(convertXPixelToModel(ev.getX()), convertYPixelToModel(ev.getY()));
1282             }
1283           }
1284         }
1285 
1286         @Override
1287         public void mouseDragged(MouseEvent ev) {
1288           if (isEnabled()) {
1289             mouseMoved(ev);
1290           }
1291         }
1292 
1293         @Override
1294         public void mouseReleased(MouseEvent ev) {
1295           if (isEnabled() && !ev.isPopupTrigger() && SwingUtilities.isLeftMouseButton(ev)) {
1296             controller.releaseMouse(convertXPixelToModel(ev.getX()), convertYPixelToModel(ev.getY()));
1297 
1298             // Restore Alt release event behavior
1299             if (windowsAltPostProcessor != null) {
1300               KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventPostProcessor(windowsAltPostProcessor);
1301               windowsAltPostProcessor = null;
1302             }
1303           }
1304         }
1305       };
1306     addMouseListener(mouseListener);
1307     addMouseMotionListener(mouseListener);
1308     addMouseWheelListener(new MouseWheelListener() {
1309         public void mouseWheelMoved(MouseWheelEvent ev) {
1310           if (ev.getModifiers() == getToolkit().getMenuShortcutKeyMask()) {
1311             float mouseX = 0;
1312             float mouseY = 0;
1313             int deltaX = 0;
1314             int deltaY = 0;
1315             if (getParent() instanceof JViewport) {
1316               mouseX = convertXPixelToModel(ev.getX());
1317               mouseY = convertYPixelToModel(ev.getY());
1318               Rectangle viewRectangle = ((JViewport)getParent()).getViewRect();
1319               deltaX = ev.getX() - viewRectangle.x;
1320               deltaY = ev.getY() - viewRectangle.y;
1321             }
1322 
1323             float oldScale = getScale();
1324             controller.zoom((float)(ev.getWheelRotation() < 0
1325                 ? Math.pow(1.05, -ev.getWheelRotation())
1326                 : Math.pow(0.95, ev.getWheelRotation())));
1327 
1328             if (getScale() != oldScale && getParent() instanceof JViewport) {
1329               // If scale changed, update viewport position to keep the same coordinates under mouse cursor
1330               ((JViewport)getParent()).setViewPosition(new Point());
1331               moveView(mouseX - convertXPixelToModel(deltaX), mouseY - convertYPixelToModel(deltaY));
1332             }
1333           } else if (getMouseWheelListeners().length == 1) {
1334             // If this listener is the only one registered on this component
1335             // redispatch event to its parent (for default scroll bar management)
1336             getParent().dispatchEvent(
1337               new MouseWheelEvent(getParent(), ev.getID(), ev.getWhen(),
1338                   ev.getModifiersEx() | ev.getModifiers(),
1339                   ev.getX() - getX(), ev.getY() - getY(),
1340                   ev.getClickCount(), ev.isPopupTrigger(), ev.getScrollType(),
1341                   ev.getScrollAmount(), ev.getWheelRotation()));
1342           }
1343         }
1344       });
1345   }
1346 
1347   /**
1348    * Adds AWT focus listener to this component that calls back <code>controller</code>
1349    * escape method on focus lost event.
1350    */
addFocusListener(final PlanController controller)1351   private void addFocusListener(final PlanController controller) {
1352     addFocusListener(new FocusAdapter() {
1353         @Override
1354         public void focusLost(FocusEvent ev) {
1355           controller.escape();
1356 
1357           // Restore Alt release event behavior
1358           if (windowsAltPostProcessor != null) {
1359             KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventPostProcessor(windowsAltPostProcessor);
1360             windowsAltPostProcessor = null;
1361           }
1362         }
1363       });
1364 
1365     if (OperatingSystem.isMacOSXLeopardOrSuperior()) {
1366       addPropertyChangeListener("Frame.active", new PropertyChangeListener() {
1367           public void propertyChange(PropertyChangeEvent ev) {
1368             if (!home.getSelectedItems().isEmpty()) {
1369               // Repaint to update selection color
1370               repaint();
1371             }
1372           }
1373         });
1374     }
1375   }
1376 
1377   /**
1378    * Adds a listener to the controller to follow changes in base plan modification state.
1379    */
addControllerListener(final PlanController controller)1380   private void addControllerListener(final PlanController controller) {
1381     controller.addPropertyChangeListener(PlanController.Property.BASE_PLAN_MODIFICATION_STATE,
1382         new PropertyChangeListener() {
1383           public void propertyChange(PropertyChangeEvent ev) {
1384             boolean wallsDoorsOrWindowsModification = controller.isBasePlanModificationState();
1385             if (wallsDoorsOrWindowsModification) {
1386               // Limit base plan modification state to walls creation/handling and doors or windows handling
1387               if (controller.getMode() != PlanController.Mode.WALL_CREATION) {
1388                 for (Selectable item : (draggedItemsFeedback != null ? draggedItemsFeedback : home.getSelectedItems())) {
1389                   if (!(item instanceof Wall)
1390                       && !(item instanceof HomePieceOfFurniture && ((HomePieceOfFurniture)item).isDoorOrWindow())) {
1391                     wallsDoorsOrWindowsModification = false;
1392                   }
1393                 }
1394               }
1395             }
1396             if (PlanComponent.this.wallsDoorsOrWindowsModification != wallsDoorsOrWindowsModification) {
1397               PlanComponent.this.wallsDoorsOrWindowsModification = wallsDoorsOrWindowsModification;
1398               repaint();
1399             }
1400           }
1401         });
1402   }
1403 
1404   /**
1405    * Installs default keys bound to actions.
1406    */
installDefaultKeyboardActions()1407   private void installDefaultKeyboardActions() {
1408     InputMap inputMap = getInputMap(WHEN_FOCUSED);
1409     inputMap.clear();
1410     inputMap.put(KeyStroke.getKeyStroke("DELETE"), ActionType.DELETE_SELECTION);
1411     inputMap.put(KeyStroke.getKeyStroke("BACK_SPACE"), ActionType.DELETE_SELECTION);
1412     inputMap.put(KeyStroke.getKeyStroke("ESCAPE"), ActionType.ESCAPE);
1413     inputMap.put(KeyStroke.getKeyStroke("shift ESCAPE"), ActionType.ESCAPE);
1414     inputMap.put(KeyStroke.getKeyStroke("LEFT"), ActionType.MOVE_SELECTION_LEFT);
1415     inputMap.put(KeyStroke.getKeyStroke("shift LEFT"), ActionType.MOVE_SELECTION_FAST_LEFT);
1416     inputMap.put(KeyStroke.getKeyStroke("UP"), ActionType.MOVE_SELECTION_UP);
1417     inputMap.put(KeyStroke.getKeyStroke("shift UP"), ActionType.MOVE_SELECTION_FAST_UP);
1418     inputMap.put(KeyStroke.getKeyStroke("DOWN"), ActionType.MOVE_SELECTION_DOWN);
1419     inputMap.put(KeyStroke.getKeyStroke("shift DOWN"), ActionType.MOVE_SELECTION_FAST_DOWN);
1420     inputMap.put(KeyStroke.getKeyStroke("RIGHT"), ActionType.MOVE_SELECTION_RIGHT);
1421     inputMap.put(KeyStroke.getKeyStroke("shift RIGHT"), ActionType.MOVE_SELECTION_FAST_RIGHT);
1422     inputMap.put(KeyStroke.getKeyStroke("ENTER"), ActionType.ACTIVATE_EDITIION);
1423     inputMap.put(KeyStroke.getKeyStroke("shift ENTER"), ActionType.ACTIVATE_EDITIION);
1424 
1425     if (OperatingSystem.isMacOSX()) {
1426       // Under Mac OS X, duplication with Alt key
1427       inputMap.put(KeyStroke.getKeyStroke("alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
1428       inputMap.put(KeyStroke.getKeyStroke("released ALT"), ActionType.DEACTIVATE_DUPLICATION);
1429       inputMap.put(KeyStroke.getKeyStroke("shift alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
1430       inputMap.put(KeyStroke.getKeyStroke("shift released ALT"), ActionType.DEACTIVATE_DUPLICATION);
1431       inputMap.put(KeyStroke.getKeyStroke("meta alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
1432       inputMap.put(KeyStroke.getKeyStroke("meta released ALT"), ActionType.DEACTIVATE_DUPLICATION);
1433       inputMap.put(KeyStroke.getKeyStroke("shift meta alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
1434       inputMap.put(KeyStroke.getKeyStroke("shift meta released ALT"), ActionType.DEACTIVATE_DUPLICATION);
1435       inputMap.put(KeyStroke.getKeyStroke("alt ESCAPE"), ActionType.ESCAPE);
1436       inputMap.put(KeyStroke.getKeyStroke("alt ENTER"), ActionType.ACTIVATE_EDITIION);
1437     } else {
1438       // Under other systems, duplication with Ctrl key
1439       inputMap.put(KeyStroke.getKeyStroke("control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
1440       inputMap.put(KeyStroke.getKeyStroke("released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
1441       inputMap.put(KeyStroke.getKeyStroke("shift control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
1442       inputMap.put(KeyStroke.getKeyStroke("shift released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
1443       inputMap.put(KeyStroke.getKeyStroke("meta control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
1444       inputMap.put(KeyStroke.getKeyStroke("meta released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
1445       inputMap.put(KeyStroke.getKeyStroke("shift meta control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
1446       inputMap.put(KeyStroke.getKeyStroke("shift meta released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
1447       inputMap.put(KeyStroke.getKeyStroke("control ESCAPE"), ActionType.ESCAPE);
1448       inputMap.put(KeyStroke.getKeyStroke("control ENTER"), ActionType.ACTIVATE_EDITIION);
1449     }
1450 
1451     if (OperatingSystem.isWindows()) {
1452       // Under Windows, magnetism toggled with Alt key
1453       inputMap.put(KeyStroke.getKeyStroke("alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
1454       inputMap.put(KeyStroke.getKeyStroke("released ALT"), ActionType.TOGGLE_MAGNETISM_OFF);
1455       inputMap.put(KeyStroke.getKeyStroke("shift alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
1456       inputMap.put(KeyStroke.getKeyStroke("shift released ALT"), ActionType.TOGGLE_MAGNETISM_OFF);
1457       inputMap.put(KeyStroke.getKeyStroke("control alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
1458       inputMap.put(KeyStroke.getKeyStroke("control released ALT"), ActionType.TOGGLE_MAGNETISM_OFF);
1459       inputMap.put(KeyStroke.getKeyStroke("shift control alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
1460       inputMap.put(KeyStroke.getKeyStroke("shift control released ALT"), ActionType.TOGGLE_MAGNETISM_OFF);
1461       inputMap.put(KeyStroke.getKeyStroke("alt ESCAPE"), ActionType.ESCAPE);
1462       inputMap.put(KeyStroke.getKeyStroke("alt ENTER"), ActionType.ACTIVATE_EDITIION);
1463     } else if (OperatingSystem.isMacOSX()) {
1464       // Under Windows, magnetism toggled with cmd key
1465       inputMap.put(KeyStroke.getKeyStroke("meta pressed META"), ActionType.TOGGLE_MAGNETISM_ON);
1466       inputMap.put(KeyStroke.getKeyStroke("released META"), ActionType.TOGGLE_MAGNETISM_OFF);
1467       inputMap.put(KeyStroke.getKeyStroke("shift meta pressed META"), ActionType.TOGGLE_MAGNETISM_ON);
1468       inputMap.put(KeyStroke.getKeyStroke("shift released META"), ActionType.TOGGLE_MAGNETISM_OFF);
1469       inputMap.put(KeyStroke.getKeyStroke("alt meta pressed META"), ActionType.TOGGLE_MAGNETISM_ON);
1470       inputMap.put(KeyStroke.getKeyStroke("alt released META"), ActionType.TOGGLE_MAGNETISM_OFF);
1471       inputMap.put(KeyStroke.getKeyStroke("shift alt meta pressed META"), ActionType.TOGGLE_MAGNETISM_ON);
1472       inputMap.put(KeyStroke.getKeyStroke("shift alt released META"), ActionType.TOGGLE_MAGNETISM_OFF);
1473       inputMap.put(KeyStroke.getKeyStroke("meta ESCAPE"), ActionType.ESCAPE);
1474       inputMap.put(KeyStroke.getKeyStroke("meta ENTER"), ActionType.ACTIVATE_EDITIION);
1475     } else {
1476       // Under other Unix systems, magnetism toggled with Alt + Shift key
1477       inputMap.put(KeyStroke.getKeyStroke("shift alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
1478       inputMap.put(KeyStroke.getKeyStroke("alt shift pressed SHIFT"), ActionType.TOGGLE_MAGNETISM_ON);
1479       inputMap.put(KeyStroke.getKeyStroke("alt released SHIFT"), ActionType.TOGGLE_MAGNETISM_OFF);
1480       inputMap.put(KeyStroke.getKeyStroke("shift released ALT"), ActionType.TOGGLE_MAGNETISM_OFF);
1481       inputMap.put(KeyStroke.getKeyStroke("control shift alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
1482       inputMap.put(KeyStroke.getKeyStroke("control alt shift pressed SHIFT"), ActionType.TOGGLE_MAGNETISM_ON);
1483       inputMap.put(KeyStroke.getKeyStroke("control alt released SHIFT"), ActionType.TOGGLE_MAGNETISM_OFF);
1484       inputMap.put(KeyStroke.getKeyStroke("control shift released ALT"), ActionType.TOGGLE_MAGNETISM_OFF);
1485       inputMap.put(KeyStroke.getKeyStroke("alt shift ESCAPE"), ActionType.ESCAPE);
1486       inputMap.put(KeyStroke.getKeyStroke("alt shift  ENTER"), ActionType.ACTIVATE_EDITIION);
1487       inputMap.put(KeyStroke.getKeyStroke("control alt shift ESCAPE"), ActionType.ESCAPE);
1488       inputMap.put(KeyStroke.getKeyStroke("control alt shift  ENTER"), ActionType.ACTIVATE_EDITIION);
1489     }
1490 
1491     inputMap.put(KeyStroke.getKeyStroke("shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
1492     inputMap.put(KeyStroke.getKeyStroke("released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
1493     if (OperatingSystem.isWindows()) {
1494       inputMap.put(KeyStroke.getKeyStroke("control shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
1495       inputMap.put(KeyStroke.getKeyStroke("control released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
1496       inputMap.put(KeyStroke.getKeyStroke("alt shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
1497       inputMap.put(KeyStroke.getKeyStroke("alt released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
1498 
1499     } else if (OperatingSystem.isMacOSX()) {
1500       inputMap.put(KeyStroke.getKeyStroke("alt shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
1501       inputMap.put(KeyStroke.getKeyStroke("alt released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
1502       inputMap.put(KeyStroke.getKeyStroke("meta shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
1503       inputMap.put(KeyStroke.getKeyStroke("meta released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
1504     } else {
1505       inputMap.put(KeyStroke.getKeyStroke("control shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
1506       inputMap.put(KeyStroke.getKeyStroke("control released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
1507       inputMap.put(KeyStroke.getKeyStroke("shift released ALT"), ActionType.ACTIVATE_ALIGNMENT);
1508       inputMap.put(KeyStroke.getKeyStroke("control shift released ALT"), ActionType.ACTIVATE_ALIGNMENT);
1509     }
1510   }
1511 
1512   /**
1513    * Installs keys bound to actions during edition.
1514    */
installEditionKeyboardActions()1515   private void installEditionKeyboardActions() {
1516     InputMap inputMap = getInputMap(WHEN_FOCUSED);
1517     inputMap.clear();
1518     inputMap.put(KeyStroke.getKeyStroke("ESCAPE"), ActionType.ESCAPE);
1519     inputMap.put(KeyStroke.getKeyStroke("shift ESCAPE"), ActionType.ESCAPE);
1520     inputMap.put(KeyStroke.getKeyStroke("ENTER"), ActionType.DEACTIVATE_EDITIION);
1521     inputMap.put(KeyStroke.getKeyStroke("shift ENTER"), ActionType.DEACTIVATE_EDITIION);
1522     if (OperatingSystem.isMacOSX()) {
1523       // Under Mac OS X, duplication with Alt key
1524       inputMap.put(KeyStroke.getKeyStroke("alt ESCAPE"), ActionType.ESCAPE);
1525       inputMap.put(KeyStroke.getKeyStroke("alt ENTER"), ActionType.DEACTIVATE_EDITIION);
1526       inputMap.put(KeyStroke.getKeyStroke("alt shift ENTER"), ActionType.DEACTIVATE_EDITIION);
1527       inputMap.put(KeyStroke.getKeyStroke("alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
1528       inputMap.put(KeyStroke.getKeyStroke("released ALT"), ActionType.DEACTIVATE_DUPLICATION);
1529       inputMap.put(KeyStroke.getKeyStroke("shift alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
1530       inputMap.put(KeyStroke.getKeyStroke("shift released ALT"), ActionType.DEACTIVATE_DUPLICATION);
1531     } else {
1532       // Under other systems, duplication with Ctrl key
1533       inputMap.put(KeyStroke.getKeyStroke("control ESCAPE"), ActionType.ESCAPE);
1534       inputMap.put(KeyStroke.getKeyStroke("control ENTER"), ActionType.DEACTIVATE_EDITIION);
1535       inputMap.put(KeyStroke.getKeyStroke("control shift ENTER"), ActionType.DEACTIVATE_EDITIION);
1536       inputMap.put(KeyStroke.getKeyStroke("control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
1537       inputMap.put(KeyStroke.getKeyStroke("released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
1538       inputMap.put(KeyStroke.getKeyStroke("shift control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
1539       inputMap.put(KeyStroke.getKeyStroke("shift released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
1540     }
1541   }
1542 
1543   /**
1544    * Creates actions that calls back <code>controller</code> methods.
1545    */
createActions(final PlanController controller)1546   private void createActions(final PlanController controller) {
1547     // Delete selection action
1548     Action deleteSelectionAction = new AbstractAction() {
1549       public void actionPerformed(ActionEvent ev) {
1550         controller.deleteSelection();
1551       }
1552     };
1553     // Escape action
1554     Action escapeAction = new AbstractAction() {
1555       public void actionPerformed(ActionEvent ev) {
1556         controller.escape();
1557       }
1558     };
1559     // Move selection action
1560     class MoveSelectionAction extends AbstractAction {
1561       private final int dx;
1562       private final int dy;
1563 
1564       public MoveSelectionAction(int dx, int dy) {
1565         this.dx = dx;
1566         this.dy = dy;
1567       }
1568 
1569       public void actionPerformed(ActionEvent ev) {
1570         controller.moveSelection(this.dx / getScale(), this.dy / getScale());
1571       }
1572     }
1573     // Toggle magnetism action
1574     class ToggleMagnetismAction extends AbstractAction {
1575       private final boolean toggle;
1576 
1577       public ToggleMagnetismAction(boolean toggle) {
1578         this.toggle = toggle;
1579       }
1580 
1581       public void actionPerformed(ActionEvent ev) {
1582         controller.toggleMagnetism(this.toggle);
1583       }
1584     }
1585     // Alignment action
1586     class SetAlignmentActivatedAction extends AbstractAction {
1587       private final boolean alignmentActivated;
1588 
1589       public SetAlignmentActivatedAction(boolean alignmentActivated) {
1590         this.alignmentActivated = alignmentActivated;
1591       }
1592 
1593       public void actionPerformed(ActionEvent ev) {
1594         controller.setAlignmentActivated(this.alignmentActivated);
1595       }
1596     }
1597     // Duplication action
1598     class SetDuplicationActivatedAction extends AbstractAction {
1599       private final boolean duplicationActivated;
1600 
1601       public SetDuplicationActivatedAction(boolean duplicationActivated) {
1602         this.duplicationActivated = duplicationActivated;
1603       }
1604 
1605       public void actionPerformed(ActionEvent ev) {
1606         controller.setDuplicationActivated(this.duplicationActivated);
1607       }
1608     }
1609     // Edition action
1610     class SetEditionActivatedAction extends AbstractAction {
1611       private final boolean editionActivated;
1612 
1613       public SetEditionActivatedAction(boolean editionActivated) {
1614         this.editionActivated = editionActivated;
1615       }
1616 
1617       public void actionPerformed(ActionEvent ev) {
1618         controller.setEditionActivated(this.editionActivated);
1619       }
1620     }
1621     ActionMap actionMap = getActionMap();
1622     actionMap.put(ActionType.DELETE_SELECTION, deleteSelectionAction);
1623     actionMap.put(ActionType.ESCAPE, escapeAction);
1624     actionMap.put(ActionType.MOVE_SELECTION_LEFT, new MoveSelectionAction(-1, 0));
1625     actionMap.put(ActionType.MOVE_SELECTION_FAST_LEFT, new MoveSelectionAction(-10, 0));
1626     actionMap.put(ActionType.MOVE_SELECTION_UP, new MoveSelectionAction(0, -1));
1627     actionMap.put(ActionType.MOVE_SELECTION_FAST_UP, new MoveSelectionAction(0, -10));
1628     actionMap.put(ActionType.MOVE_SELECTION_DOWN, new MoveSelectionAction(0, 1));
1629     actionMap.put(ActionType.MOVE_SELECTION_FAST_DOWN, new MoveSelectionAction(0, 10));
1630     actionMap.put(ActionType.MOVE_SELECTION_RIGHT, new MoveSelectionAction(1, 0));
1631     actionMap.put(ActionType.MOVE_SELECTION_FAST_RIGHT, new MoveSelectionAction(10, 0));
1632     actionMap.put(ActionType.TOGGLE_MAGNETISM_ON, new ToggleMagnetismAction(true));
1633     actionMap.put(ActionType.TOGGLE_MAGNETISM_OFF, new ToggleMagnetismAction(false));
1634     actionMap.put(ActionType.ACTIVATE_ALIGNMENT, new SetAlignmentActivatedAction(true));
1635     actionMap.put(ActionType.DEACTIVATE_ALIGNMENT, new SetAlignmentActivatedAction(false));
1636     actionMap.put(ActionType.ACTIVATE_DUPLICATION, new SetDuplicationActivatedAction(true));
1637     actionMap.put(ActionType.DEACTIVATE_DUPLICATION, new SetDuplicationActivatedAction(false));
1638     actionMap.put(ActionType.ACTIVATE_EDITIION, new SetEditionActivatedAction(true));
1639     actionMap.put(ActionType.DEACTIVATE_EDITIION, new SetEditionActivatedAction(false));
1640   }
1641 
1642   /**
1643    * Creates the text fields used in tool tip and their label.
1644    */
createToolTipTextFields(UserPreferences preferences, final PlanController controller)1645   private void createToolTipTextFields(UserPreferences preferences,
1646                                        final PlanController controller) {
1647     this.toolTipEditableTextFields = new HashMap<PlanController.EditableProperty, JFormattedTextField>();
1648     Font toolTipFont = UIManager.getFont("ToolTip.font");
1649     for (final PlanController.EditableProperty editableProperty : PlanController.EditableProperty.values()) {
1650       final JFormattedTextField textField = new JFormattedTextField() {
1651           @Override
1652           public Dimension getPreferredSize() {
1653             // Enlarge preferred size of one pixel
1654             Dimension preferredSize = super.getPreferredSize();
1655             return new Dimension(preferredSize.width + 1, preferredSize.height);
1656           }
1657         };
1658       updateToolTipTextFieldFormatterFactory(textField, editableProperty, preferences);
1659       textField.setFont(toolTipFont);
1660       textField.setOpaque(false);
1661       textField.setBorder(null);
1662       if (controller != null) {
1663         // Add a listener to notify changes to controller
1664         textField.getDocument().addDocumentListener(new DocumentListener() {
1665             public void changedUpdate(DocumentEvent ev) {
1666               try {
1667                 textField.commitEdit();
1668                 controller.updateEditableProperty(editableProperty, textField.getValue());
1669               } catch (ParseException ex) {
1670                 controller.updateEditableProperty(editableProperty, null);
1671               }
1672             }
1673 
1674             public void insertUpdate(DocumentEvent ev) {
1675               changedUpdate(ev);
1676             }
1677 
1678             public void removeUpdate(DocumentEvent ev) {
1679               changedUpdate(ev);
1680             }
1681           });
1682       }
1683 
1684       this.toolTipEditableTextFields.put(editableProperty, textField);
1685     }
1686   }
1687 
updateToolTipTextFieldFormatterFactory(JFormattedTextField textField, PlanController.EditableProperty editableProperty, UserPreferences preferences)1688   private static void updateToolTipTextFieldFormatterFactory(JFormattedTextField textField,
1689                                                              PlanController.EditableProperty editableProperty,
1690                                                              UserPreferences preferences) {
1691     InternationalFormatter formatter;
1692     if (editableProperty == PlanController.EditableProperty.ANGLE) {
1693       DecimalFormat format = new DecimalFormat("0.#");
1694       try {
1695         format = new CalculatorFormat(format, null);
1696       } catch (LinkageError ex) {
1697         // Don't allow math expressions if Jeks Parser library isn't available
1698       }
1699       formatter = new NumberFormatter(format);
1700     } else {
1701       Format lengthFormat = preferences.getLengthUnit().getFormat();
1702       if (lengthFormat instanceof DecimalFormat) {
1703         try {
1704           lengthFormat = new CalculatorFormat((DecimalFormat)lengthFormat, preferences.getLengthUnit());
1705         } catch (LinkageError ex) {
1706           // Don't allow math expressions if Jeks Parser library isn't available
1707         }
1708         formatter = new NumberFormatter((DecimalFormat)lengthFormat);
1709       } else {
1710         formatter = new InternationalFormatter(lengthFormat);
1711       }
1712     }
1713     textField.setFormatterFactory(new DefaultFormatterFactory(formatter));
1714   }
1715 
1716   /**
1717    * Returns a custom cursor with a hot spot point at center of cursor.
1718    */
createCustomCursor(String smallCursorImageResource, String largeCursorImageResource, String cursorName, int defaultCursor)1719   private Cursor createCustomCursor(String smallCursorImageResource,
1720                                     String largeCursorImageResource,
1721                                     String cursorName,
1722                                     int    defaultCursor) {
1723     if (OperatingSystem.isMacOSX()) {
1724       smallCursorImageResource = smallCursorImageResource.replace(".png", "-macosx.png");
1725     }
1726     return createCustomCursor(PlanComponent.class.getResource(smallCursorImageResource),
1727         PlanComponent.class.getResource(largeCursorImageResource),
1728         0.5f, 0.5f, cursorName,
1729         Cursor.getPredefinedCursor(defaultCursor));
1730   }
1731 
1732   /**
1733    * Returns a custom cursor created from images in parameters.
1734    */
createCustomCursor(URL smallCursorImageUrl, URL largeCursorImageUrl, float xCursorHotSpot, float yCursorHotSpot, String cursorName, Cursor defaultCursor)1735   protected Cursor createCustomCursor(URL smallCursorImageUrl,
1736                                       URL largeCursorImageUrl,
1737                                       float xCursorHotSpot,
1738                                       float yCursorHotSpot,
1739                                       String cursorName,
1740                                       Cursor defaultCursor) {
1741     return SwingTools.createCustomCursor(smallCursorImageUrl, largeCursorImageUrl,
1742         xCursorHotSpot, yCursorHotSpot, cursorName, defaultCursor);
1743   }
1744 
1745   /**
1746    * Returns the preferred size of this component.
1747    */
1748   @Override
getPreferredSize()1749   public Dimension getPreferredSize() {
1750     if (isPreferredSizeSet()) {
1751       return super.getPreferredSize();
1752     } else {
1753       Insets insets = getInsets();
1754       Rectangle2D planBounds = getPlanBounds();
1755       return new Dimension(
1756           convertLengthToPixel(planBounds.getWidth() + MARGIN * 2) + insets.left + insets.right,
1757           convertLengthToPixel(planBounds.getHeight() + MARGIN * 2) + insets.top + insets.bottom);
1758     }
1759   }
1760 
1761   /**
1762    * Returns the bounds of the plan displayed by this component.
1763    */
getPlanBounds()1764   private Rectangle2D getPlanBounds() {
1765     if (!this.planBoundsCacheValid) {
1766       // Always enlarge plan bounds only when plan component is a child of a scroll pane
1767       if (this.planBoundsCache == null
1768           || !(getParent() instanceof JViewport)) {
1769         // Ensure plan bounds are 10 x 10 meters wide at minimum
1770         this.planBoundsCache = new Rectangle2D.Float(0, 0, 1000, 1000);
1771       }
1772       // Enlarge plan bounds to include background images, home bounds and observer camera
1773       if (this.backgroundImageCache != null) {
1774         BackgroundImage backgroundImage = this.home.getBackgroundImage();
1775         if (backgroundImage != null) {
1776           this.planBoundsCache.add(-backgroundImage.getXOrigin(), -backgroundImage.getYOrigin());
1777           this.planBoundsCache.add(this.backgroundImageCache.getWidth() * backgroundImage.getScale() - backgroundImage.getXOrigin(),
1778               this.backgroundImageCache.getHeight() * backgroundImage.getScale() - backgroundImage.getYOrigin());
1779         }
1780         for (Level level : this.home.getLevels()) {
1781           BackgroundImage levelBackgroundImage = level.getBackgroundImage();
1782           if (levelBackgroundImage != null) {
1783             this.planBoundsCache.add(-levelBackgroundImage.getXOrigin(), -levelBackgroundImage.getYOrigin());
1784             this.planBoundsCache.add(this.backgroundImageCache.getWidth() * levelBackgroundImage.getScale() - levelBackgroundImage.getXOrigin(),
1785                 this.backgroundImageCache.getHeight() * levelBackgroundImage.getScale() - levelBackgroundImage.getYOrigin());
1786           }
1787         }
1788       }
1789       Graphics2D g = (Graphics2D)getGraphics();
1790       if (g != null) {
1791         setRenderingHints(g);
1792       }
1793       Rectangle2D homeItemsBounds = getItemsBounds(g, getPaintedItems());
1794       if (homeItemsBounds != null) {
1795         this.planBoundsCache.add(homeItemsBounds);
1796       }
1797       for (float [] point : this.home.getObserverCamera().getPoints()) {
1798         this.planBoundsCache.add(point [0], point [1]);
1799       }
1800       this.planBoundsCacheValid = true;
1801     }
1802     return this.planBoundsCache;
1803   }
1804 
1805   /**
1806    * Returns the collection of walls, furniture, rooms and dimension lines of the home
1807    * painted by this component wherever the level they belong to is selected or not.
1808    */
getPaintedItems()1809   protected List<Selectable> getPaintedItems() {
1810     return this.home.getSelectableViewableItems();
1811   }
1812 
1813   /**
1814    * Returns the bounds of the given collection of <code>items</code>.
1815    */
getItemsBounds(Graphics g, Collection<? extends Selectable> items)1816   private Rectangle2D getItemsBounds(Graphics g, Collection<? extends Selectable> items) {
1817     Rectangle2D itemsBounds = null;
1818     for (Selectable item : items) {
1819       if (itemsBounds == null) {
1820         itemsBounds = getItemBounds(g, item);
1821       } else {
1822         itemsBounds.add(getItemBounds(g, item));
1823       }
1824     }
1825     return itemsBounds;
1826   }
1827 
1828   /**
1829    * Returns the bounds of the given <code>item</code>.
1830    */
getItemBounds(Graphics g, Selectable item)1831   protected Rectangle2D getItemBounds(Graphics g, Selectable item) {
1832     // Add to bounds all the visible items
1833     float [][] points = item.getPoints();
1834     Rectangle2D itemBounds = new Rectangle2D.Float(points [0][0], points [0][1], 0, 0);
1835     for (int i = 1; i < points.length; i++) {
1836       itemBounds.add(points [i][0], points [i][1]);
1837     }
1838 
1839     // Retrieve used font
1840     Font componentFont;
1841     if (g != null) {
1842       componentFont = g.getFont();
1843     } else {
1844       componentFont = getFont();
1845     }
1846 
1847     if (item instanceof Room) {
1848       // Add to bounds the displayed name and area bounds of each room
1849       Room room = (Room)item;
1850       float xRoomCenter = room.getXCenter();
1851       float yRoomCenter = room.getYCenter();
1852       String roomName = room.getName();
1853       if (roomName != null && roomName.length() > 0) {
1854         addTextBounds(room.getClass(),
1855             roomName, room.getNameStyle(),
1856             xRoomCenter + room.getNameXOffset(),
1857             yRoomCenter + room.getNameYOffset(), room.getNameAngle(), itemBounds);
1858       }
1859       if (room.isAreaVisible()) {
1860         float area = room.getArea();
1861         if (area > 0.01f) {
1862           String areaText = this.preferences.getLengthUnit().getAreaFormatWithUnit().format(area);
1863           addTextBounds(room.getClass(),
1864               areaText, room.getAreaStyle(),
1865               xRoomCenter + room.getAreaXOffset(),
1866               yRoomCenter + room.getAreaYOffset(), room.getAreaAngle(), itemBounds);
1867         }
1868       }
1869     } else if (item instanceof Polyline) {
1870       Polyline polyline = (Polyline)item;
1871       return ShapeTools.getPolylineShape(polyline.getPoints(),
1872           polyline.getJoinStyle() == Polyline.JoinStyle.CURVED, polyline.isClosedPath()).getBounds2D();
1873     } else if (item instanceof HomePieceOfFurniture) {
1874       if (item instanceof HomeDoorOrWindow) {
1875         HomeDoorOrWindow doorOrWindow = (HomeDoorOrWindow)item;
1876         // Add to bounds door and window sashes
1877         for (Sash sash : doorOrWindow.getSashes()) {
1878           itemBounds.add(getDoorOrWindowSashShape(doorOrWindow, sash).getBounds2D());
1879         }
1880       } else if (item instanceof HomeFurnitureGroup) {
1881         itemBounds.add(getItemsBounds(g, ((HomeFurnitureGroup)item).getFurniture()));
1882       }
1883       // Add to bounds the displayed name of the piece of furniture
1884       HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
1885       String pieceName = piece.getName();
1886       if (piece.isVisible()
1887           && piece.isNameVisible()
1888           && pieceName.length() > 0) {
1889         addTextBounds(piece.getClass(),
1890             pieceName, piece.getNameStyle(),
1891             piece.getX() + piece.getNameXOffset(),
1892             piece.getY() + piece.getNameYOffset(), piece.getNameAngle(), itemBounds);
1893       }
1894     } else if (item instanceof DimensionLine) {
1895       // Add to bounds the text bounds of the dimension line length
1896       DimensionLine dimensionLine = (DimensionLine)item;
1897       float dimensionLineLength = dimensionLine.getLength();
1898       String lengthText = this.preferences.getLengthUnit().getFormat().format(dimensionLineLength);
1899       TextStyle lengthStyle = dimensionLine.getLengthStyle();
1900       if (lengthStyle == null) {
1901         lengthStyle = this.preferences.getDefaultTextStyle(dimensionLine.getClass());
1902       }
1903       FontMetrics lengthFontMetrics = getFontMetrics(componentFont, lengthStyle);
1904       Rectangle2D lengthTextBounds = lengthFontMetrics.getStringBounds(lengthText, g);
1905       // Transform length text bounding rectangle corners to their real location
1906       double angle = Math.atan2(dimensionLine.getYEnd() - dimensionLine.getYStart(),
1907           dimensionLine.getXEnd() - dimensionLine.getXStart());
1908       AffineTransform transform = AffineTransform.getTranslateInstance(
1909           dimensionLine.getXStart(), dimensionLine.getYStart());
1910       transform.rotate(angle);
1911       transform.translate(0, dimensionLine.getOffset());
1912       transform.translate((dimensionLineLength - lengthTextBounds.getWidth()) / 2,
1913           dimensionLine.getOffset() <= 0
1914               ? -lengthFontMetrics.getDescent() - 1
1915               : lengthFontMetrics.getAscent() + 1);
1916       GeneralPath lengthTextBoundsPath = new GeneralPath(lengthTextBounds);
1917       for (PathIterator it = lengthTextBoundsPath.getPathIterator(transform); !it.isDone(); it.next()) {
1918         float [] pathPoint = new float[2];
1919         if (it.currentSegment(pathPoint) != PathIterator.SEG_CLOSE) {
1920           itemBounds.add(pathPoint [0], pathPoint [1]);
1921         }
1922       }
1923       // Add to bounds the end lines drawn at dimension line start and end
1924       transform.setToTranslation(dimensionLine.getXStart(), dimensionLine.getYStart());
1925       transform.rotate(angle);
1926       transform.translate(0, dimensionLine.getOffset());
1927       for (PathIterator it = DIMENSION_LINE_END.getPathIterator(transform); !it.isDone(); it.next()) {
1928         float [] pathPoint = new float[2];
1929         if (it.currentSegment(pathPoint) != PathIterator.SEG_CLOSE) {
1930           itemBounds.add(pathPoint [0], pathPoint [1]);
1931         }
1932       }
1933       transform.translate(dimensionLineLength, 0);
1934       for (PathIterator it = DIMENSION_LINE_END.getPathIterator(transform); !it.isDone(); it.next()) {
1935         float [] pathPoint = new float[2];
1936         if (it.currentSegment(pathPoint) != PathIterator.SEG_CLOSE) {
1937           itemBounds.add(pathPoint [0], pathPoint [1]);
1938         }
1939       }
1940     } else if (item instanceof Label) {
1941       // Add to bounds the displayed text of a label
1942       Label label = (Label)item;
1943       addTextBounds(label.getClass(),
1944           label.getText(), label.getStyle(), label.getX(), label.getY(), label.getAngle(), itemBounds);
1945     } else if (item instanceof Compass) {
1946       Compass compass = (Compass)item;
1947       AffineTransform transform = AffineTransform.getTranslateInstance(compass.getX(), compass.getY());
1948       transform.scale(compass.getDiameter(), compass.getDiameter());
1949       transform.rotate(compass.getNorthDirection());
1950       return COMPASS.createTransformedShape(transform).getBounds2D();
1951     }
1952     return itemBounds;
1953   }
1954 
1955   /**
1956    * Add <code>text</code> bounds to the given rectangle <code>bounds</code>.
1957    */
addTextBounds(Class<? extends Selectable> selectableClass, String text, TextStyle style, float x, float y, float angle, Rectangle2D bounds)1958   private void addTextBounds(Class<? extends Selectable> selectableClass,
1959                              String text, TextStyle style,
1960                              float x, float y, float angle,
1961                              Rectangle2D bounds) {
1962     if (style == null) {
1963       style = this.preferences.getDefaultTextStyle(selectableClass);
1964     }
1965     for (float [] points : getTextBounds(text, style, x, y, angle)) {
1966       bounds.add(points [0], points [1]);
1967     }
1968   }
1969 
1970   /**
1971    * Returns the coordinates of the bounding rectangle of the <code>text</code> centered at
1972    * the point (<code>x</code>,<code>y</code>).
1973    */
getTextBounds(String text, TextStyle style, float x, float y, float angle)1974   public float [][] getTextBounds(String text, TextStyle style,
1975                                   float x, float y, float angle) {
1976     FontMetrics fontMetrics = getFontMetrics(getFont(), style);
1977     Rectangle2D textBounds = null;
1978     String [] lines = text.split("\n");
1979     Graphics2D g = (Graphics2D)getGraphics();
1980     if (g != null) {
1981       setRenderingHints(g);
1982     }
1983     for (int i = 0; i < lines.length; i++) {
1984       Rectangle2D lineBounds = fontMetrics.getStringBounds(lines [i], g);
1985       if (textBounds == null
1986           || textBounds.getWidth() < lineBounds.getWidth()) {
1987         textBounds = lineBounds;
1988       }
1989     }
1990     float textWidth = (float)textBounds.getWidth();
1991     float shiftX;
1992     if (style.getAlignment() == TextStyle.Alignment.LEFT) {
1993       shiftX = 0;
1994     } else if (style.getAlignment() == TextStyle.Alignment.RIGHT) {
1995       shiftX = -textWidth;
1996     } else { // CENTER
1997       shiftX = -textWidth / 2;
1998     }
1999     if (angle == 0) {
2000       float minY = (float)(y + textBounds.getY());
2001       float maxY = (float)(minY + textBounds.getHeight());
2002       minY -= (float)(textBounds.getHeight() * (lines.length - 1));
2003       return new float [][] {
2004           {x + shiftX, minY},
2005           {x + shiftX + textWidth, minY},
2006           {x + shiftX + textWidth, maxY},
2007           {x + shiftX, maxY}};
2008     } else {
2009       textBounds.add(textBounds.getX(), textBounds.getY() - textBounds.getHeight() * (lines.length - 1));
2010       // Transform text bounding rectangle corners to their real location
2011       AffineTransform transform = new AffineTransform();
2012       transform.translate(x, y);
2013       transform.rotate(angle);
2014       transform.translate(shiftX, 0);
2015       GeneralPath textBoundsPath = new GeneralPath(textBounds);
2016       List<float []> textPoints = new ArrayList<float[]>(4);
2017       for (PathIterator it = textBoundsPath.getPathIterator(transform); !it.isDone(); it.next()) {
2018         float [] pathPoint = new float[2];
2019         if (it.currentSegment(pathPoint) != PathIterator.SEG_CLOSE) {
2020           textPoints.add(pathPoint);
2021         }
2022       }
2023       return textPoints.toArray(new float [textPoints.size()][]);
2024     }
2025   }
2026 
2027   /**
2028    * Returns the AWT font matching a given text style.
2029    */
getFont(Font defaultFont, TextStyle textStyle)2030   protected Font getFont(Font defaultFont, TextStyle textStyle) {
2031     if (this.fonts == null) {
2032       this.fonts = new WeakHashMap<TextStyle, Font>();
2033     }
2034     Font font = this.fonts.get(textStyle);
2035     if (font == null) {
2036       int fontStyle = Font.PLAIN;
2037       if (textStyle.isBold()) {
2038         fontStyle = Font.BOLD;
2039       }
2040       if (textStyle.isItalic()) {
2041         fontStyle |= Font.ITALIC;
2042       }
2043       if (defaultFont == null
2044           || this.preferences.getDefaultFontName() != null
2045           || textStyle.getFontName() != null) {
2046         String fontName = textStyle.getFontName();
2047         if (fontName == null) {
2048           fontName = this.preferences.getDefaultFontName();
2049         }
2050         defaultFont = new Font(fontName, fontStyle, 1);
2051       }
2052       font = defaultFont.deriveFont(fontStyle, textStyle.getFontSize());
2053       this.fonts.put(textStyle, font);
2054     }
2055     return font;
2056   }
2057 
2058   /**
2059    * Returns the font metrics matching a given text style.
2060    */
getFontMetrics(Font defaultFont, TextStyle textStyle)2061   protected FontMetrics getFontMetrics(Font defaultFont, TextStyle textStyle) {
2062     if (this.fontsMetrics == null) {
2063       this.fontsMetrics = new WeakHashMap<TextStyle, FontMetrics>();
2064     }
2065     FontMetrics fontMetrics = this.fontsMetrics.get(textStyle);
2066     if (fontMetrics == null) {
2067       fontMetrics = getFontMetrics(getFont(defaultFont, textStyle));
2068       this.fontsMetrics.put(textStyle, fontMetrics);
2069     }
2070     return fontMetrics;
2071   }
2072 
2073   /**
2074    * Sets whether plan's background should be painted or not.
2075    * Background may include grid and an image.
2076    */
setBackgroundPainted(boolean backgroundPainted)2077   public void setBackgroundPainted(boolean backgroundPainted) {
2078     if (this.backgroundPainted != backgroundPainted) {
2079       this.backgroundPainted = backgroundPainted;
2080       repaint();
2081     }
2082   }
2083 
2084   /**
2085    * Returns <code>true</code> if plan's background should be painted.
2086    */
isBackgroundPainted()2087   public boolean isBackgroundPainted() {
2088     return this.backgroundPainted;
2089   }
2090 
2091   /**
2092    * Sets whether the outline of home selected items should be painted or not.
2093    */
setSelectedItemsOutlinePainted(boolean selectedItemsOutlinePainted)2094   public void setSelectedItemsOutlinePainted(boolean selectedItemsOutlinePainted) {
2095     if (this.selectedItemsOutlinePainted != selectedItemsOutlinePainted) {
2096       this.selectedItemsOutlinePainted = selectedItemsOutlinePainted;
2097       repaint();
2098     }
2099   }
2100 
2101   /**
2102    * Returns <code>true</code> if the outline of home selected items should be painted.
2103    */
isSelectedItemsOutlinePainted()2104   public boolean isSelectedItemsOutlinePainted() {
2105     return this.selectedItemsOutlinePainted;
2106   }
2107 
2108   /**
2109    * Paints this component.
2110    */
2111   @Override
paintComponent(Graphics g)2112   protected void paintComponent(Graphics g) {
2113     Graphics2D g2D = (Graphics2D)g.create();
2114     if (this.backgroundPainted) {
2115       paintBackground(g2D, getBackgroundColor(PaintMode.PAINT));
2116     }
2117     Insets insets = getInsets();
2118     // Clip component to avoid drawing in empty borders
2119     g2D.clipRect(insets.left, insets.top,
2120         getWidth() - insets.left - insets.right,
2121         getHeight() - insets.top - insets.bottom);
2122     // Change component coordinates system to plan system
2123     Rectangle2D planBounds = getPlanBounds();
2124     float scale = getScale() * this.resolutionScale;
2125     g2D.translate(insets.left + (MARGIN - planBounds.getMinX()) * scale,
2126         insets.top + (MARGIN - planBounds.getMinY()) * scale);
2127     g2D.scale(scale, scale);
2128     setRenderingHints(g2D);
2129     try {
2130       paintContent(g2D, getScale(), PaintMode.PAINT);
2131     } catch (InterruptedIOException ex) {
2132       // Ignore exception because it may happen only in EXPORT paint mode
2133     }
2134     g2D.dispose();
2135   }
2136 
2137   /**
2138    * Returns the print preferred scale of the plan drawn in this component
2139    * to make it fill <code>pageFormat</code> imageable size.
2140    */
getPrintPreferredScale(Graphics g, PageFormat pageFormat)2141   public float getPrintPreferredScale(Graphics g, PageFormat pageFormat) {
2142     return getPrintPreferredScale(LengthUnit.inchToCentimeter((float)pageFormat.getImageableWidth() / 72),
2143         LengthUnit.inchToCentimeter((float)pageFormat.getImageableHeight() / 72));
2144   }
2145 
2146   /**
2147    * Returns the preferred scale to ensure it can be fully printed on the given print zone.
2148    */
getPrintPreferredScale(float preferredWidth, float preferredHeight)2149   public float getPrintPreferredScale(float preferredWidth, float preferredHeight) {
2150     List<Selectable> printedItems = getPaintedItems();
2151     Graphics2D g = (Graphics2D)getGraphics();
2152     if (g != null) {
2153       setRenderingHints(g);
2154     }
2155     Rectangle2D printedItemBounds = getItemsBounds(g, printedItems);
2156     if (printedItemBounds != null) {
2157       float extraMargin = getStrokeWidthExtraMargin(printedItems, PaintMode.PRINT);
2158       // Compute the largest integer scale possible
2159       int scaleInverse = (int)Math.ceil(Math.max(
2160           (printedItemBounds.getWidth() + 2 * extraMargin) / preferredWidth,
2161           (printedItemBounds.getHeight() + 2 * extraMargin) / preferredHeight));
2162       return 1f / scaleInverse;
2163     } else {
2164       return 0;
2165     }
2166   }
2167 
2168   /**
2169    * Returns the margin that should be added around home items bounds to ensure their
2170    * line stroke width is always fully visible.
2171    */
getStrokeWidthExtraMargin(List<Selectable> items, PaintMode paintMode)2172   private float getStrokeWidthExtraMargin(List<Selectable> items, PaintMode paintMode) {
2173     float extraMargin = BORDER_STROKE_WIDTH;
2174     if (Home.getFurnitureSubList(items).size() > 0) {
2175       extraMargin = Math.max(extraMargin, getStrokeWidth(HomePieceOfFurniture.class, paintMode));
2176     }
2177     if (Home.getWallsSubList(items).size() > 0) {
2178       extraMargin = Math.max(extraMargin, getStrokeWidth(Wall.class, paintMode));
2179     }
2180     if (Home.getRoomsSubList(items).size() > 0) {
2181       extraMargin = Math.max(extraMargin, getStrokeWidth(Room.class, paintMode));
2182     }
2183     List<Polyline> polylines = Home.getPolylinesSubList(items);
2184     if (polylines.size() > 0) {
2185       for (Polyline polyline : polylines) {
2186         extraMargin = Math.max(extraMargin, polyline.getStartArrowStyle() != null ||  polyline.getEndArrowStyle() != null
2187             ? 1.5f * polyline.getThickness()
2188             : polyline.getThickness());
2189       }
2190     }
2191     if (Home.getDimensionLinesSubList(items).size() > 0) {
2192       extraMargin = Math.max(extraMargin, getStrokeWidth(DimensionLine.class, paintMode));
2193     }
2194     return extraMargin / 2;
2195   }
2196 
2197   /**
2198    * Returns the stroke width used to paint an item of the given class.
2199    */
getStrokeWidth(Class<? extends Selectable> itemClass, PaintMode paintMode)2200   private float getStrokeWidth(Class<? extends Selectable> itemClass, PaintMode paintMode) {
2201     float strokeWidth;
2202     if (Wall.class.isAssignableFrom(itemClass)
2203         || Room.class.isAssignableFrom(itemClass)) {
2204       strokeWidth = WALL_STROKE_WIDTH;
2205     } else {
2206       strokeWidth = BORDER_STROKE_WIDTH;
2207     }
2208     if (paintMode == PaintMode.PRINT) {
2209       strokeWidth *= 0.5;
2210     }
2211     return strokeWidth;
2212   }
2213 
2214   /**
2215    * Prints this component plan at the scale given in the home print attributes or at a scale
2216    * that makes it fill <code>pageFormat</code> imageable size if this attribute is <code>null</code>.
2217    */
print(Graphics g, PageFormat pageFormat, int pageIndex)2218   public int print(Graphics g, PageFormat pageFormat, int pageIndex) {
2219     List<Selectable> printedItems = getPaintedItems();
2220     Rectangle2D printedItemBounds = getItemsBounds(g, printedItems);
2221     if (printedItemBounds != null) {
2222       double imageableX = pageFormat.getImageableX();
2223       double imageableY = pageFormat.getImageableY();
2224       double imageableWidth = pageFormat.getImageableWidth();
2225       double imageableHeight = pageFormat.getImageableHeight();
2226       float printScale;
2227       float rowIndex;
2228       float columnIndex;
2229       int pagesPerRow;
2230       int pagesPerColumn;
2231       if (this.home.getPrint() == null || this.home.getPrint().getPlanScale() == null) {
2232         // Compute a scale that ensures the plan will fill the component if plan scale is null
2233         printScale = getPrintPreferredScale(g, pageFormat) * LengthUnit.centimeterToInch(72);
2234         if (pageIndex > 0) {
2235           return NO_SUCH_PAGE;
2236         }
2237         pagesPerRow = 1;
2238         pagesPerColumn = 1;
2239         rowIndex   = 0;
2240         columnIndex = 0;
2241       } else {
2242         // Apply print scale to paper size expressed in 1/72nds of an inch
2243         printScale = this.home.getPrint().getPlanScale().floatValue() * LengthUnit.centimeterToInch(72);
2244         pagesPerRow = (int)(printedItemBounds.getWidth() * printScale / imageableWidth);
2245         if (printedItemBounds.getWidth() * printScale != imageableWidth) {
2246           pagesPerRow++;
2247         }
2248         pagesPerColumn = (int)(printedItemBounds.getHeight() * printScale / imageableHeight);
2249         if (printedItemBounds.getHeight() * printScale != imageableHeight) {
2250           pagesPerColumn++;
2251         }
2252         if (pageIndex >= pagesPerRow * pagesPerColumn) {
2253           return NO_SUCH_PAGE;
2254         }
2255         rowIndex = pageIndex / pagesPerRow;
2256         columnIndex = pageIndex - rowIndex * pagesPerRow;
2257       }
2258 
2259       Graphics2D g2D = (Graphics2D)g.create();
2260       g2D.clip(new Rectangle2D.Double(imageableX, imageableY, imageableWidth, imageableHeight));
2261       // Change coordinates system to paper imageable origin
2262       g2D.translate(imageableX - columnIndex * imageableWidth, imageableY - rowIndex * imageableHeight);
2263       g2D.scale(printScale, printScale);
2264       float extraMargin = getStrokeWidthExtraMargin(printedItems, PaintMode.PRINT);
2265       g2D.translate(-printedItemBounds.getMinX() + extraMargin,
2266           -printedItemBounds.getMinY() + extraMargin);
2267       // Center plan in component if possible
2268       g2D.translate(Math.max(0,
2269               (imageableWidth * pagesPerRow / printScale - printedItemBounds.getWidth() - 2 * extraMargin) / 2),
2270           Math.max(0,
2271               (imageableHeight * pagesPerColumn / printScale - printedItemBounds.getHeight() - 2 * extraMargin) / 2));
2272       setRenderingHints(g2D);
2273       try {
2274         // Print component contents
2275         paintContent(g2D, printScale, PaintMode.PRINT);
2276       } catch (InterruptedIOException ex) {
2277         // Ignore exception because it may happen only in EXPORT paint mode
2278       }
2279       g2D.dispose();
2280       return PAGE_EXISTS;
2281     } else {
2282       return NO_SUCH_PAGE;
2283     }
2284   }
2285 
2286   /**
2287    * Returns an image of selected items in plan for transfer purpose.
2288    */
createTransferData(DataType dataType)2289   public Object createTransferData(DataType dataType) {
2290     if (dataType == DataType.PLAN_IMAGE) {
2291       return getClipboardImage();
2292     } else {
2293       return null;
2294     }
2295   }
2296 
2297   /**
2298    * Returns an image of the selected items displayed by this component
2299    * (camera excepted) with no outline at scale 1/1 (1 pixel = 1cm)
2300    * or at a smaller scale if image is larger than 100m x 100m
2301    * or if free memory is missing.
2302    */
getClipboardImage()2303   public BufferedImage getClipboardImage() {
2304     // Create an image that contains only selected items
2305     Rectangle2D selectionBounds = getSelectionBounds(false);
2306     if (selectionBounds == null) {
2307       return null;
2308     } else {
2309       // Use a scale of 1 except if image is very large or free memory is missing
2310       float clipboardScale = 1f;
2311       while (clipboardScale > 1 / 1024f
2312              && (Runtime.getRuntime().freeMemory() < 4 * clipboardScale * clipboardScale * selectionBounds.getWidth() * selectionBounds.getHeight()
2313                  || clipboardScale * clipboardScale * selectionBounds.getWidth() * selectionBounds.getHeight() > 1E8)) {
2314         clipboardScale /= 2f;
2315       }
2316       float extraMargin = getStrokeWidthExtraMargin(this.home.getSelectedItems(), PaintMode.CLIPBOARD);
2317       BufferedImage image = new BufferedImage((int)Math.ceil(selectionBounds.getWidth() * clipboardScale + 2 * extraMargin),
2318               (int)Math.ceil(selectionBounds.getHeight() * clipboardScale + 2 * extraMargin), BufferedImage.TYPE_INT_RGB);
2319       Graphics2D g2D = (Graphics2D)image.getGraphics();
2320       // Paint background in white
2321       g2D.setColor(Color.WHITE);
2322       g2D.fillRect(0, 0, image.getWidth(), image.getHeight());
2323       // Change component coordinates system to plan system
2324       g2D.scale(clipboardScale, clipboardScale);
2325       g2D.translate(-selectionBounds.getMinX() + extraMargin,
2326           -selectionBounds.getMinY() + extraMargin);
2327       setRenderingHints(g2D);
2328       try {
2329         // Paint component contents
2330         paintContent(g2D, clipboardScale, PaintMode.CLIPBOARD);
2331       } catch (InterruptedIOException ex) {
2332         // Ignore exception because it may happen only in EXPORT paint mode
2333         return null;
2334       }
2335       g2D.dispose();
2336       return image;
2337     }
2338   }
2339 
2340   /**
2341    * Returns <code>true</code> if the given format is SVG.
2342    */
isFormatTypeSupported(FormatType formatType)2343   public boolean isFormatTypeSupported(FormatType formatType) {
2344     return formatType == FormatType.SVG;
2345   }
2346 
2347   /**
2348    * Writes this plan in the given output stream at SVG (Scalable Vector Graphics) format if this is the requested format.
2349    */
exportData(OutputStream out, FormatType formatType, Properties settings)2350   public void exportData(OutputStream out, FormatType formatType, Properties settings) throws IOException {
2351     if  (formatType == FormatType.SVG) {
2352       exportToSVG(out);
2353     } else {
2354       throw new UnsupportedOperationException("Unsupported format " + formatType);
2355     }
2356   }
2357 
2358   /**
2359    * Writes this plan in the given output stream at SVG (Scalable Vector Graphics) format.
2360    */
exportToSVG(OutputStream out)2361   public void exportToSVG(OutputStream out) throws IOException {
2362     SVGSupport.exportToSVG(out, this);
2363   }
2364 
2365   /**
2366    * Separated static class to be able to exclude FreeHEP library from classpath
2367    * in case the application doesn't use export to SVG format.
2368    */
2369   private static class SVGSupport {
exportToSVG(OutputStream out, PlanComponent planComponent)2370     public static void exportToSVG(OutputStream out,
2371                                    PlanComponent planComponent) throws IOException {
2372       List<Selectable> homeItems = planComponent.getPaintedItems();
2373       Rectangle2D svgItemBounds = planComponent.getItemsBounds(null, homeItems);
2374       if (svgItemBounds == null) {
2375         svgItemBounds = new Rectangle2D.Float();
2376       }
2377 
2378       float svgScale = 1f;
2379       float extraMargin = planComponent.getStrokeWidthExtraMargin(homeItems, PaintMode.EXPORT);
2380       Dimension imageSize = new Dimension((int)Math.ceil(svgItemBounds.getWidth() * svgScale + 2 * extraMargin),
2381           (int)Math.ceil(svgItemBounds.getHeight() * svgScale + 2 * extraMargin));
2382 
2383       SVGGraphics2D exportG2D = new SVGGraphics2D(out, imageSize) {
2384           @Override
2385           public void writeHeader() throws IOException {
2386             // Use English locale to avoid wrong encoding when localized dates contain accentuated letters
2387             Locale defaultLocale = Locale.getDefault();
2388             Locale.setDefault(Locale.ENGLISH);
2389             super.writeHeader();
2390             Locale.setDefault(defaultLocale);
2391           }
2392         };
2393       UserProperties properties = new UserProperties();
2394       properties.setProperty(SVGGraphics2D.STYLABLE, true);
2395       properties.setProperty(SVGGraphics2D.WRITE_IMAGES_AS, ImageConstants.PNG);
2396       properties.setProperty(SVGGraphics2D.TITLE,
2397           planComponent.home.getName() != null
2398               ? planComponent.home.getName()
2399               : "" );
2400       properties.setProperty(SVGGraphics2D.FOR, System.getProperty("user.name", ""));
2401       exportG2D.setProperties(properties);
2402       exportG2D.startExport();
2403       exportG2D.translate(-svgItemBounds.getMinX() + extraMargin,
2404           -svgItemBounds.getMinY() + extraMargin);
2405 
2406       planComponent.checkCurrentThreadIsntInterrupted(PaintMode.EXPORT);
2407       planComponent.paintContent(exportG2D, svgScale, PaintMode.EXPORT);
2408       exportG2D.endExport();
2409     }
2410   }
2411 
2412   /**
2413    * Throws an <code>InterruptedIOException</code> exception if current thread
2414    * is interrupted and <code>paintMode</code> is equal to <code>PaintMode.EXPORT</code>.
2415    */
checkCurrentThreadIsntInterrupted(PaintMode paintMode)2416   private void checkCurrentThreadIsntInterrupted(PaintMode paintMode) throws InterruptedIOException {
2417     if (paintMode == PaintMode.EXPORT
2418         && Thread.interrupted()) {
2419       throw new InterruptedIOException("Current thread interrupted");
2420     }
2421   }
2422 
2423   /**
2424    * Sets rendering hints used to paint plan.
2425    */
setRenderingHints(Graphics2D g2D)2426   private void setRenderingHints(Graphics2D g2D) {
2427     g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
2428     g2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
2429     g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2430     g2D.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
2431   }
2432 
2433   /**
2434    * Fills the background.
2435    */
paintBackground(Graphics2D g2D, Color backgroundColor)2436   private void paintBackground(Graphics2D g2D, Color backgroundColor) {
2437     if (isOpaque()) {
2438       g2D.setColor(backgroundColor);
2439       g2D.fillRect(0, 0, getWidth(), getHeight());
2440     }
2441   }
2442 
2443   /**
2444    * Paints background image and returns <code>true</code> if an image is painted.
2445    */
paintBackgroundImage(Graphics2D g2D, PaintMode paintMode)2446   private boolean paintBackgroundImage(Graphics2D g2D, PaintMode paintMode) {
2447     Level selectedLevel = this.home.getSelectedLevel();
2448     Level backgroundImageLevel = null;
2449     if (selectedLevel != null) {
2450       // Search the first level at same elevation with a background image
2451       List<Level> levels = this.home.getLevels();
2452       for (int i = levels.size() - 1; i >= 0; i--) {
2453         Level level = levels.get(i);
2454         if (level.getElevation() == selectedLevel.getElevation()
2455             && level.getElevationIndex() <= selectedLevel.getElevationIndex()
2456             && level.isViewable()
2457             && level.getBackgroundImage() != null
2458             && level.getBackgroundImage().isVisible()) {
2459           backgroundImageLevel = level;
2460           break;
2461         }
2462       }
2463     }
2464     final BackgroundImage backgroundImage = backgroundImageLevel == null
2465         ? this.home.getBackgroundImage()
2466         : backgroundImageLevel.getBackgroundImage();
2467     if (backgroundImage != null && backgroundImage.isVisible()) {
2468       // Under Mac OS X, prepare background image with alpha because Java 5/6 doesn't always
2469       // paint images correctly with alpha, and Java 7 blocks for some images
2470       final boolean prepareBackgroundImageWithAlphaInMemory = OperatingSystem.isMacOSX();
2471       if (this.backgroundImageCache == null && paintMode == PaintMode.PAINT) {
2472         // Load background image in an executor
2473         if (backgroundImageLoader == null) {
2474           backgroundImageLoader = Executors.newSingleThreadExecutor();
2475         }
2476         backgroundImageLoader.execute(new Runnable() {
2477             public void run() {
2478               if (backgroundImageCache == null) {
2479                 backgroundImageCache = readBackgroundImage(backgroundImage.getImage(), prepareBackgroundImageWithAlphaInMemory);
2480                 revalidate();
2481               }
2482             }
2483           });
2484       } else {
2485         // Paint image at specified scale with 0.7 alpha
2486         AffineTransform previousTransform = g2D.getTransform();
2487         g2D.translate(-backgroundImage.getXOrigin(), -backgroundImage.getYOrigin());
2488         float backgroundImageScale = backgroundImage.getScale();
2489         g2D.scale(backgroundImageScale, backgroundImageScale);
2490         Composite oldComposite = null;
2491         if (!prepareBackgroundImageWithAlphaInMemory) {
2492           oldComposite = setTransparency(g2D, 0.7f);
2493         }
2494         g2D.drawImage(this.backgroundImageCache != null
2495             ? this.backgroundImageCache
2496             : readBackgroundImage(backgroundImage.getImage(), prepareBackgroundImageWithAlphaInMemory), 0, 0, this);
2497         if (!prepareBackgroundImageWithAlphaInMemory) {
2498           g2D.setComposite(oldComposite);
2499         }
2500         g2D.setTransform(previousTransform);
2501       }
2502       return true;
2503     }
2504     return false;
2505   }
2506 
2507   /**
2508    * Returns the foreground color used to draw content.
2509    */
getForegroundColor(PaintMode mode)2510   protected Color getForegroundColor(PaintMode mode) {
2511     if (mode == PaintMode.PAINT) {
2512       return getForeground();
2513     } else {
2514       return Color.BLACK;
2515     }
2516   }
2517 
2518   /**
2519    * Returns the background color used to draw content.
2520    */
getBackgroundColor(PaintMode mode)2521   protected Color getBackgroundColor(PaintMode mode) {
2522     if (mode == PaintMode.PAINT) {
2523       return getBackground();
2524     } else {
2525       return Color.WHITE;
2526     }
2527   }
2528 
2529   /**
2530    * Returns the image contained in <code>imageContent</code> or an empty image if reading failed.
2531    */
readBackgroundImage(Content imageContent, boolean prepareBackgroundImageWithAlpha)2532   private BufferedImage readBackgroundImage(Content imageContent, boolean prepareBackgroundImageWithAlpha) {
2533     InputStream contentStream = null;
2534     try {
2535       try {
2536         contentStream = imageContent.openStream();
2537         BufferedImage image = ImageIO.read(contentStream);
2538         if (prepareBackgroundImageWithAlpha) {
2539           BufferedImage backgroundImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
2540           Graphics2D g2D = (Graphics2D)backgroundImage.getGraphics();
2541           g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));
2542           g2D.drawRenderedImage(image, null);
2543           g2D.dispose();
2544           return backgroundImage;
2545         } else {
2546           return image;
2547         }
2548       } finally {
2549         if (contentStream != null) {
2550           contentStream.close();
2551         }
2552       }
2553     } catch (IOException ex) {
2554       return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
2555       // Ignore exceptions, the user may know its background image is incorrect
2556       // if he tries to modify the background image
2557     }
2558   }
2559 
2560   /**
2561    * Paints walls and rooms of lower levels or upper levels to help the user draw in the selected level.
2562    */
paintOtherLevels(Graphics2D g2D, float planScale, Color backgroundColor, Color foregroundColor)2563   private void paintOtherLevels(Graphics2D g2D, float planScale,
2564                                 Color backgroundColor, Color foregroundColor) {
2565     List<Level> levels = this.home.getLevels();
2566     Level selectedLevel = this.home.getSelectedLevel();
2567     if (levels.size() > 1
2568         && selectedLevel != null) {
2569       boolean level0 = levels.get(0).getElevation() == selectedLevel.getElevation();
2570       List<Level> otherLevels = null;
2571       if (this.otherLevelsRoomsCache == null
2572           || this.otherLevelsWallsCache == null) {
2573         int selectedLevelIndex = levels.indexOf(selectedLevel);
2574         otherLevels = new ArrayList<Level>();
2575         if (level0) {
2576           // Search levels at the same elevation above level0
2577           int nextElevationLevelIndex = selectedLevelIndex;
2578           while (++nextElevationLevelIndex < levels.size()
2579               && levels.get(nextElevationLevelIndex).getElevation() == selectedLevel.getElevation()) {
2580           }
2581           if (nextElevationLevelIndex < levels.size()) {
2582             Level nextLevel = levels.get(nextElevationLevelIndex);
2583             float nextElevation = nextLevel.getElevation();
2584             do {
2585               if (nextLevel.isViewable()) {
2586                 otherLevels.add(nextLevel);
2587               }
2588             } while (++nextElevationLevelIndex < levels.size()
2589                 && (nextLevel = levels.get(nextElevationLevelIndex)).getElevation() == nextElevation);
2590           }
2591         } else {
2592           // Search levels at the same elevation below level0
2593           int previousElevationLevelIndex = selectedLevelIndex;
2594           while (--previousElevationLevelIndex >= 0
2595               && levels.get(previousElevationLevelIndex).getElevation() == selectedLevel.getElevation()) {
2596           }
2597           if (previousElevationLevelIndex >= 0) {
2598             Level previousLevel = levels.get(previousElevationLevelIndex);
2599             float previousElevation = previousLevel.getElevation();
2600             do {
2601               if (previousLevel.isViewable()) {
2602                 otherLevels.add(previousLevel);
2603               }
2604             } while (--previousElevationLevelIndex >= 0
2605                 && (previousLevel = levels.get(previousElevationLevelIndex)).getElevation() == previousElevation);
2606           }
2607         }
2608 
2609         if (this.otherLevelsRoomsCache == null) {
2610           if (!otherLevels.isEmpty()) {
2611             // Search viewable floors in levels above level0 or ceilings in levels below level0
2612             List<Room> otherLevelsRooms = new ArrayList<Room>();
2613             for (Room room : this.home.getRooms()) {
2614               for (Level otherLevel : otherLevels) {
2615                 if (room.getLevel() == otherLevel
2616                     && (level0 && room.isFloorVisible()
2617                         || !level0 && room.isCeilingVisible())) {
2618                   otherLevelsRooms.add(room);
2619                 }
2620               }
2621             }
2622             if (otherLevelsRooms.size() > 0) {
2623               this.otherLevelsRoomAreaCache = getItemsArea(otherLevelsRooms);
2624               this.otherLevelsRoomsCache = otherLevelsRooms;
2625             }
2626           }
2627           if (this.otherLevelsRoomsCache == null) {
2628             this.otherLevelsRoomsCache = Collections.emptyList();
2629           }
2630         }
2631 
2632         if (this.otherLevelsWallsCache == null) {
2633           if (!otherLevels.isEmpty()) {
2634             // Search viewable walls in other levels
2635             List<Wall> otherLevelswalls = new ArrayList<Wall>();
2636             for (Wall wall : this.home.getWalls()) {
2637               if (!isViewableAtSelectedLevel(wall)) {
2638                 for (Level otherLevel : otherLevels) {
2639                   if (wall.getLevel() == otherLevel) {
2640                     otherLevelswalls.add(wall);
2641                   }
2642                 }
2643               }
2644             }
2645             if (otherLevelswalls.size() > 0) {
2646               this.otherLevelsWallAreaCache = getItemsArea(otherLevelswalls);
2647               this.otherLevelsWallsCache = otherLevelswalls;
2648             }
2649           }
2650         }
2651         if (this.otherLevelsWallsCache == null) {
2652           this.otherLevelsWallsCache = Collections.emptyList();
2653         }
2654       }
2655 
2656       if (!this.otherLevelsRoomsCache.isEmpty()) {
2657         Composite oldComposite = setTransparency(g2D,
2658             this.preferences.isGridVisible() ? 0.2f : 0.1f);
2659         g2D.setPaint(Color.GRAY);
2660         g2D.fill(this.otherLevelsRoomAreaCache);
2661         g2D.setComposite(oldComposite);
2662       }
2663 
2664       if (!this.otherLevelsWallsCache.isEmpty()) {
2665         Composite oldComposite = setTransparency(g2D,
2666             this.preferences.isGridVisible() ? 0.2f : 0.1f);
2667         fillAndDrawWallsArea(g2D, this.otherLevelsWallAreaCache, planScale,
2668             getWallPaint(planScale, backgroundColor, foregroundColor, this.preferences.getNewWallPattern()),
2669             foregroundColor, PaintMode.PAINT);
2670         g2D.setComposite(oldComposite);
2671       }
2672     }
2673   }
2674 
2675   /**
2676    * Sets the transparency composite to the given percentage and returns the old composite.
2677    */
setTransparency(Graphics2D g2D, float alpha)2678   private Composite setTransparency(Graphics2D g2D, float alpha) {
2679     Composite oldComposite = g2D.getComposite();
2680     if (oldComposite instanceof AlphaComposite) {
2681       g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
2682         ((AlphaComposite)oldComposite).getAlpha() * alpha));
2683     } else {
2684       g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
2685     }
2686     return oldComposite;
2687   }
2688 
2689   /**
2690    * Paints background grid lines.
2691    */
paintGrid(Graphics2D g2D, float gridScale)2692   private void paintGrid(Graphics2D g2D, float gridScale) {
2693     float gridSize = getGridSize(gridScale);
2694     float mainGridSize = getMainGridSize(gridScale);
2695 
2696     float xMin;
2697     float yMin;
2698     float xMax;
2699     float yMax;
2700     Rectangle2D planBounds = getPlanBounds();
2701     if (getParent() instanceof JViewport) {
2702       Rectangle viewRectangle = ((JViewport)getParent()).getViewRect();
2703       xMin = convertXPixelToModel(viewRectangle.x - 1);
2704       yMin = convertYPixelToModel(viewRectangle.y - 1);
2705       xMax = convertXPixelToModel(viewRectangle.x + viewRectangle.width);
2706       yMax = convertYPixelToModel(viewRectangle.y + viewRectangle.height);
2707     } else {
2708       xMin = (float)planBounds.getMinX() - MARGIN;
2709       yMin = (float)planBounds.getMinY() - MARGIN;
2710       xMax = convertXPixelToModel(getWidth());
2711       yMax = convertYPixelToModel(getHeight());
2712     }
2713     boolean useGridImage = false;
2714     try {
2715       useGridImage = OperatingSystem.isMacOSX()
2716           && System.getProperty("apple.awt.graphics.UseQuartz", "false").equals("false");
2717     } catch (AccessControlException ex) {
2718       // Unsigned applet
2719     }
2720     if (useGridImage) {
2721       // Draw grid with an image texture under Mac OS X, because default 2D rendering engine
2722       // is too slow and can't be replaced by Quartz engine in applet environment
2723       int imageWidth = Math.round(mainGridSize * gridScale * this.resolutionScale);
2724       BufferedImage gridImage = new BufferedImage(imageWidth, imageWidth, BufferedImage.TYPE_INT_ARGB);
2725       Graphics2D imageGraphics = (Graphics2D)gridImage.getGraphics();
2726       setRenderingHints(imageGraphics);
2727       imageGraphics.scale(gridScale * this.resolutionScale, gridScale * this.resolutionScale);
2728 
2729       paintGridLines(imageGraphics, gridScale, 0, mainGridSize, 0, mainGridSize, gridSize, mainGridSize);
2730       imageGraphics.dispose();
2731 
2732       g2D.setPaint(new TexturePaint(gridImage, new Rectangle2D.Float(0, 0, mainGridSize, mainGridSize)));
2733 
2734       g2D.fill(new Rectangle2D.Float(xMin, yMin, xMax - xMin, yMax - yMin));
2735     } else {
2736       paintGridLines(g2D, gridScale, xMin, xMax, yMin, yMax, gridSize, mainGridSize);
2737     }
2738   }
2739 
2740   /**
2741    * Paints background grid lines from <code>xMin</code> to <code>xMax</code>
2742    * and <code>yMin</code> to <code>yMax</code>.
2743    */
paintGridLines(Graphics2D g2D, float gridScale, float xMin, float xMax, float yMin, float yMax, float gridSize, float mainGridSize)2744   private void paintGridLines(Graphics2D g2D, float gridScale,
2745                               float xMin, float xMax, float yMin, float yMax,
2746                               float gridSize, float mainGridSize) {
2747     g2D.setColor(UIManager.getColor("controlShadow"));
2748     g2D.setStroke(new BasicStroke(0.5f / gridScale));
2749     // Draw vertical lines
2750     for (double x = (int)(xMin / gridSize) * gridSize; x < xMax; x += gridSize) {
2751       g2D.draw(new Line2D.Double(x, yMin, x, yMax));
2752     }
2753     // Draw horizontal lines
2754     for (double y = (int)(yMin / gridSize) * gridSize; y < yMax; y += gridSize) {
2755       g2D.draw(new Line2D.Double(xMin, y, xMax, y));
2756     }
2757 
2758     if (mainGridSize != gridSize) {
2759       g2D.setStroke(new BasicStroke(1.5f / gridScale,
2760           BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
2761       // Draw main vertical lines
2762       for (double x = (int)(xMin / mainGridSize) * mainGridSize; x < xMax; x += mainGridSize) {
2763         g2D.draw(new Line2D.Double(x, yMin, x, yMax));
2764       }
2765       // Draw positive main horizontal lines
2766       for (double y = (int)(yMin / mainGridSize) * mainGridSize; y < yMax; y += mainGridSize) {
2767         g2D.draw(new Line2D.Double(xMin, y, xMax, y));
2768       }
2769     }
2770   }
2771 
2772   /**
2773    * Returns the space between main lines grid.
2774    */
getMainGridSize(float gridScale)2775   private float getMainGridSize(float gridScale) {
2776     float [] mainGridSizes;
2777     LengthUnit lengthUnit = this.preferences.getLengthUnit();
2778     if (lengthUnit == LengthUnit.INCH
2779         || lengthUnit == LengthUnit.INCH_DECIMALS) {
2780       // Use a grid in inch and foot with a minimum grid increment of 1 inch
2781       float oneFoot = 2.54f * 12;
2782       mainGridSizes = new float [] {oneFoot, 3 * oneFoot, 6 * oneFoot,
2783                                     12 * oneFoot, 24 * oneFoot, 48 * oneFoot, 96 * oneFoot, 192 * oneFoot, 384 * oneFoot};
2784     } else {
2785       // Use a grid in cm and meters with a minimum grid increment of 1 cm
2786       mainGridSizes = new float [] {100, 200, 500, 1000, 2000, 5000, 10000};
2787     }
2788     // Compute grid size to get a grid where the space between each line is less than 50 pixels
2789     float mainGridSize = mainGridSizes [0];
2790     for (int i = 1; i < mainGridSizes.length && mainGridSize * gridScale < 50; i++) {
2791       mainGridSize = mainGridSizes [i];
2792     }
2793     return mainGridSize;
2794   }
2795 
2796   /**
2797    * Returns the space between lines grid.
2798    */
getGridSize(float gridScale)2799   private float getGridSize(float gridScale) {
2800     float [] gridSizes;
2801     LengthUnit lengthUnit = this.preferences.getLengthUnit();
2802     if (lengthUnit == LengthUnit.INCH
2803         || lengthUnit == LengthUnit.INCH_DECIMALS) {
2804       // Use a grid in inch and foot with a minimum grid increment of 1 inch
2805       float oneFoot = 2.54f * 12;
2806       gridSizes = new float [] {2.54f, 5.08f, 7.62f, 15.24f, oneFoot, 3 * oneFoot, 6 * oneFoot,
2807                                 12 * oneFoot, 24 * oneFoot, 48 * oneFoot, 96 * oneFoot, 192 * oneFoot, 384 * oneFoot};
2808     } else {
2809       // Use a grid in cm and meters with a minimum grid increment of 1 cm
2810       gridSizes = new float [] {1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000};
2811     }
2812     // Compute grid size to get a grid where the space between each line is less than 10 pixels
2813     float gridSize = gridSizes [0];
2814     for (int i = 1; i < gridSizes.length && gridSize * gridScale < 10; i++) {
2815       gridSize = gridSizes [i];
2816     }
2817     return gridSize;
2818   }
2819 
2820   /**
2821    * Paints plan items.
2822    * @throws InterruptedIOException if painting was interrupted (may happen only
2823    *           if <code>paintMode</code> is equal to <code>PaintMode.EXPORT</code>).
2824    */
paintContent(Graphics2D g2D, float planScale, PaintMode paintMode)2825   private void paintContent(Graphics2D g2D, float planScale, PaintMode paintMode) throws InterruptedIOException {
2826     Color backgroundColor = getBackgroundColor(paintMode);
2827     Color foregroundColor = getForegroundColor(paintMode);
2828     if (this.backgroundPainted) {
2829       paintBackgroundImage(g2D, paintMode);
2830       if (paintMode == PaintMode.PAINT) {
2831         paintOtherLevels(g2D, planScale, backgroundColor, foregroundColor);
2832         if (this.preferences.isGridVisible()) {
2833           paintGrid(g2D, planScale);
2834         }
2835       }
2836     }
2837 
2838     paintHomeItems(g2D, planScale, backgroundColor, foregroundColor, paintMode);
2839 
2840     if (paintMode == PaintMode.PAINT) {
2841       List<Selectable> selectedItems = this.home.getSelectedItems();
2842 
2843       Color selectionColor = getSelectionColor();
2844       Color furnitureOutlineColor = getFurnitureOutlineColor();
2845       Paint selectionOutlinePaint = new Color(selectionColor.getRed(), selectionColor.getGreen(),
2846           selectionColor.getBlue(), 128);
2847       Stroke selectionOutlineStroke = new BasicStroke(6 / planScale,
2848           BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
2849       Stroke dimensionLinesSelectionOutlineStroke = new BasicStroke(4 / planScale,
2850           BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
2851       Stroke locationFeedbackStroke = new BasicStroke(
2852           1 / planScale, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL, 0,
2853           new float [] {20 / planScale, 5 / planScale, 5 / planScale, 5 / planScale}, 4 / planScale);
2854 
2855       paintCamera(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor,
2856           planScale, backgroundColor, foregroundColor);
2857 
2858       // Paint alignment feedback depending on aligned object class
2859       if (this.alignedObjectClass != null) {
2860         if (Wall.class.isAssignableFrom(this.alignedObjectClass)) {
2861           paintWallAlignmentFeedback(g2D, (Wall)this.alignedObjectFeedback, this.locationFeeback, this.showPointFeedback,
2862               selectionColor, locationFeedbackStroke, planScale,
2863               selectionOutlinePaint, selectionOutlineStroke);
2864         } else if (Room.class.isAssignableFrom(this.alignedObjectClass)) {
2865           paintRoomAlignmentFeedback(g2D, (Room)this.alignedObjectFeedback, this.locationFeeback, this.showPointFeedback,
2866               selectionColor, locationFeedbackStroke, planScale,
2867               selectionOutlinePaint, selectionOutlineStroke);
2868         } else if (Polyline.class.isAssignableFrom(this.alignedObjectClass)) {
2869           if (this.showPointFeedback) {
2870             paintPointFeedback(g2D, this.locationFeeback, selectionColor, planScale, selectionOutlinePaint, selectionOutlineStroke);
2871           }
2872         } else if (DimensionLine.class.isAssignableFrom(this.alignedObjectClass)) {
2873           paintDimensionLineAlignmentFeedback(g2D, (DimensionLine)this.alignedObjectFeedback, this.locationFeeback, this.showPointFeedback,
2874               selectionColor, locationFeedbackStroke, planScale,
2875               selectionOutlinePaint, selectionOutlineStroke);
2876         }
2877       }
2878       if (this.centerAngleFeedback != null) {
2879        paintAngleFeedback(g2D, this.centerAngleFeedback, this.point1AngleFeedback, this.point2AngleFeedback,
2880            planScale, selectionColor);
2881       }
2882       if (this.dimensionLinesFeedback != null) {
2883         List<Selectable> emptySelection = Collections.emptyList();
2884         paintDimensionLines(g2D, this.dimensionLinesFeedback, emptySelection,
2885             null, null, null, locationFeedbackStroke, planScale,
2886             backgroundColor, selectionColor, paintMode, true);
2887       }
2888 
2889       if (this.draggedItemsFeedback != null) {
2890         paintDimensionLines(g2D, Home.getDimensionLinesSubList(this.draggedItemsFeedback), this.draggedItemsFeedback,
2891             selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, null,
2892             locationFeedbackStroke, planScale, backgroundColor, foregroundColor, paintMode, false);
2893         paintLabels(g2D, Home.getLabelsSubList(this.draggedItemsFeedback), this.draggedItemsFeedback,
2894             selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, null,
2895             planScale, foregroundColor, paintMode);
2896         paintRoomsOutline(g2D, this.draggedItemsFeedback, selectionOutlinePaint, selectionOutlineStroke, null,
2897             planScale, foregroundColor);
2898         paintWallsOutline(g2D, this.draggedItemsFeedback, selectionOutlinePaint, selectionOutlineStroke, null,
2899             planScale, foregroundColor);
2900         paintFurniture(g2D, Home.getFurnitureSubList(this.draggedItemsFeedback), selectedItems, planScale, null,
2901             foregroundColor, furnitureOutlineColor, paintMode, false);
2902         paintFurnitureOutline(g2D, this.draggedItemsFeedback, selectionOutlinePaint, selectionOutlineStroke, null,
2903             planScale, foregroundColor);
2904       }
2905 
2906       paintRectangleFeedback(g2D, selectionColor, planScale);
2907     }
2908   }
2909 
2910   /**
2911    * Paints home items at the given scale, and with background and foreground colors.
2912    * Outline around selected items will be painted only under <code>PAINT</code> mode.
2913    */
paintHomeItems(Graphics g, float planScale, Color backgroundColor, Color foregroundColor, PaintMode paintMode)2914   protected void paintHomeItems(Graphics g, float planScale,
2915                                 Color backgroundColor, Color foregroundColor, PaintMode paintMode) throws InterruptedIOException {
2916     Graphics2D g2D = (Graphics2D)g;
2917     List<Selectable> selectedItems = this.home.getSelectedItems();
2918     if (this.sortedLevelFurniture == null) {
2919       // Sort home furniture in elevation order
2920       this.sortedLevelFurniture = new ArrayList<HomePieceOfFurniture>();
2921       for (HomePieceOfFurniture piece : this.home.getFurniture()) {
2922         if (isViewableAtSelectedLevel(piece)) {
2923           this.sortedLevelFurniture.add(piece);
2924         }
2925       }
2926       Collections.sort(this.sortedLevelFurniture,
2927           new Comparator<HomePieceOfFurniture>() {
2928             public int compare(HomePieceOfFurniture piece1, HomePieceOfFurniture piece2) {
2929               return Float.compare(piece1.getGroundElevation(), piece2.getGroundElevation());
2930             }
2931           });
2932     }
2933 
2934     Color selectionColor = getSelectionColor();
2935     Paint selectionOutlinePaint = new Color(selectionColor.getRed(), selectionColor.getGreen(),
2936         selectionColor.getBlue(), 128);
2937     Stroke selectionOutlineStroke = new BasicStroke(6 / planScale,
2938         BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
2939     Stroke dimensionLinesSelectionOutlineStroke = new BasicStroke(4 / planScale,
2940         BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
2941     Stroke locationFeedbackStroke = new BasicStroke(
2942         1 / planScale, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL, 0,
2943         new float [] {20 / planScale, 5 / planScale, 5 / planScale, 5 / planScale}, 4 / planScale);
2944 
2945     paintCompass(g2D, selectedItems, planScale, foregroundColor, paintMode);
2946 
2947     checkCurrentThreadIsntInterrupted(paintMode);
2948     paintRooms(g2D, selectedItems, planScale, foregroundColor, paintMode);
2949 
2950     checkCurrentThreadIsntInterrupted(paintMode);
2951     paintWalls(g2D, selectedItems, planScale, backgroundColor, foregroundColor, paintMode);
2952 
2953     checkCurrentThreadIsntInterrupted(paintMode);
2954     paintFurniture(g2D, this.sortedLevelFurniture, selectedItems,
2955         planScale, backgroundColor, foregroundColor, getFurnitureOutlineColor(), paintMode, true);
2956 
2957     checkCurrentThreadIsntInterrupted(paintMode);
2958     paintPolylines(g2D, this.home.getPolylines(), selectedItems, selectionOutlinePaint,
2959         selectionColor, planScale, foregroundColor, paintMode);
2960 
2961     checkCurrentThreadIsntInterrupted(paintMode);
2962     paintDimensionLines(g2D, this.home.getDimensionLines(), selectedItems,
2963         selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, selectionColor,
2964         locationFeedbackStroke, planScale, backgroundColor, foregroundColor, paintMode, false);
2965 
2966     // Paint rooms text, furniture name and labels last to ensure they are not hidden
2967     checkCurrentThreadIsntInterrupted(paintMode);
2968     paintRoomsNameAndArea(g2D, selectedItems, planScale, foregroundColor, paintMode);
2969 
2970     checkCurrentThreadIsntInterrupted(paintMode);
2971     paintFurnitureName(g2D, this.sortedLevelFurniture, selectedItems, planScale, foregroundColor, paintMode);
2972 
2973     checkCurrentThreadIsntInterrupted(paintMode);
2974     paintLabels(g2D, this.home.getLabels(), selectedItems, selectionOutlinePaint, dimensionLinesSelectionOutlineStroke,
2975         selectionColor, planScale, foregroundColor, paintMode);
2976 
2977     if (paintMode == PaintMode.PAINT
2978         && this.selectedItemsOutlinePainted) {
2979       paintCompassOutline(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor,
2980           planScale, foregroundColor);
2981       paintRoomsOutline(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor,
2982           planScale, foregroundColor);
2983       paintWallsOutline(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor,
2984           planScale, foregroundColor);
2985       paintFurnitureOutline(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor,
2986           planScale, foregroundColor);
2987     }
2988   }
2989 
2990   /**
2991    * Returns the color used to draw selection outlines.
2992    */
getSelectionColor()2993   protected Color getSelectionColor() {
2994     return getDefaultSelectionColor(this);
2995   }
2996 
2997   /**
2998    * Returns the default color used to draw selection outlines.
2999    */
getDefaultSelectionColor(JComponent planComponent)3000   static Color getDefaultSelectionColor(JComponent planComponent) {
3001     if (OperatingSystem.isMacOSX()) {
3002       if (OperatingSystem.isMacOSXLeopardOrSuperior()) {
3003         Window window = SwingUtilities.getWindowAncestor(planComponent);
3004         if (window != null && !window.isActive()) {
3005           Color selectionColor = UIManager.getColor("List.selectionInactiveBackground");
3006           if (selectionColor != null) {
3007             return selectionColor.darker();
3008           }
3009         }
3010         Color selectionColor = UIManager.getColor("List.selectionBackground");
3011         if (selectionColor != null) {
3012           return selectionColor;
3013         }
3014       }
3015 
3016       return UIManager.getColor("textHighlight");
3017     } else {
3018       // On systems different from Mac OS X, take a darker color
3019       return UIManager.getColor("textHighlight").darker();
3020     }
3021   }
3022 
3023   /**
3024    * Returns the color used to draw furniture outline of
3025    * the shape where a user can click to select a piece of furniture.
3026    */
getFurnitureOutlineColor()3027   protected Color getFurnitureOutlineColor() {
3028     return new Color((getForeground().getRGB() & 0xFFFFFF) | 0x55000000, true);
3029   }
3030 
3031   /**
3032    * Paints rooms.
3033    */
paintRooms(Graphics2D g2D, List<Selectable> selectedItems, float planScale, Color foregroundColor, PaintMode paintMode)3034   private void paintRooms(Graphics2D g2D, List<Selectable> selectedItems, float planScale,
3035                           Color foregroundColor, PaintMode paintMode) {
3036     if (this.sortedLevelRooms == null) {
3037       // Sort home rooms in floor / floor-ceiling / ceiling order
3038       this.sortedLevelRooms = new ArrayList<Room>();
3039       for (Room room : this.home.getRooms()) {
3040         if (isViewableAtSelectedLevel(room)) {
3041           this.sortedLevelRooms.add(room);
3042         }
3043       }
3044       Collections.sort(this.sortedLevelRooms,
3045           new Comparator<Room>() {
3046             public int compare(Room room1, Room room2) {
3047               if (room1.isFloorVisible() == room2.isFloorVisible()
3048                   && room1.isCeilingVisible() == room2.isCeilingVisible()) {
3049                 return 0; // Keep default order if the rooms have the same visibility
3050               } else if (!room2.isFloorVisible()
3051                          || room2.isCeilingVisible()) {
3052                 return 1;
3053               } else {
3054                 return -1;
3055               }
3056             }
3057           });
3058     }
3059 
3060     Color defaultFillPaint = paintMode == PaintMode.PRINT
3061         ? Color.WHITE
3062         : Color.GRAY;
3063     // Draw rooms area
3064     g2D.setStroke(new BasicStroke(getStrokeWidth(Room.class, paintMode) / planScale));
3065     for (Room room : this.sortedLevelRooms) {
3066       boolean selectedRoom = selectedItems.contains(room);
3067       // In clipboard paint mode, paint room only if it is selected
3068       if (paintMode != PaintMode.CLIPBOARD
3069           || selectedRoom) {
3070         g2D.setPaint(defaultFillPaint);
3071         float textureAngle = 0;
3072         if (this.preferences.isRoomFloorColoredOrTextured()
3073             && room.isFloorVisible()) {
3074           // Use room floor color or texture image
3075           if (room.getFloorColor() != null) {
3076             g2D.setPaint(new Color(room.getFloorColor()));
3077           } else {
3078             final HomeTexture floorTexture = room.getFloorTexture();
3079             if (floorTexture != null) {
3080               if (this.floorTextureImagesCache == null) {
3081                 this.floorTextureImagesCache = new WeakHashMap<HomeTexture, BufferedImage>();
3082               }
3083               BufferedImage textureImage = this.floorTextureImagesCache.get(floorTexture);
3084               if (textureImage == null
3085                   || textureImage == WAIT_TEXTURE_IMAGE) {
3086                 final boolean waitForTexture = paintMode != PaintMode.PAINT;
3087                 if (isTextureManagerAvailable()
3088                     // Don't use images managed by Java3D textures
3089                     // to avoid InternalError "Surface not cachable" in Graphics2D#fill call
3090                     // See bug at https://bugs.openjdk.java.net/browse/JDK-8072618
3091                     && !(OperatingSystem.isLinux()
3092                           && OperatingSystem.isJavaVersionGreaterOrEqual("1.7"))) {
3093                   // Prefer to share textures images with texture manager if it's available
3094                   TextureManager.getInstance().loadTexture(floorTexture.getImage(), waitForTexture,
3095                       new TextureManager.TextureObserver() {
3096                         public void textureUpdated(Texture texture) {
3097                           floorTextureImagesCache.put(floorTexture,
3098                               ((ImageComponent2D)texture.getImage(0)).getImage());
3099                           if (!waitForTexture) {
3100                             repaint();
3101                           }
3102                         }
3103                       });
3104                 } else {
3105                   // Use icon manager if texture manager should be ignored
3106                   Icon textureIcon = IconManager.getInstance().getIcon(floorTexture.getImage(),
3107                       waitForTexture ? null : this);
3108                   if (IconManager.getInstance().isWaitIcon(textureIcon)) {
3109                     this.floorTextureImagesCache.put(floorTexture, WAIT_TEXTURE_IMAGE);
3110                   } else if (IconManager.getInstance().isErrorIcon(textureIcon)) {
3111                     this.floorTextureImagesCache.put(floorTexture, ERROR_TEXTURE_IMAGE);
3112                   } else {
3113                     BufferedImage textureIconImage = new BufferedImage(
3114                         textureIcon.getIconWidth(), textureIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
3115                     Graphics2D g2DIcon = (Graphics2D)textureIconImage.getGraphics();
3116                     textureIcon.paintIcon(this, g2DIcon, 0, 0);
3117                     g2DIcon.dispose();
3118                     this.floorTextureImagesCache.put(floorTexture, textureIconImage);
3119                   }
3120                 }
3121                 textureImage = this.floorTextureImagesCache.get(floorTexture);
3122               }
3123 
3124               float textureWidth = floorTexture.getWidth();
3125               float textureHeight = floorTexture.getHeight();
3126               if (textureWidth == -1 || textureHeight == -1) {
3127                 textureWidth = 100;
3128                 textureHeight = 100;
3129               }
3130               float textureScale = floorTexture.getScale();
3131               textureAngle = floorTexture.getAngle();
3132               double cosAngle = Math.cos(textureAngle);
3133               double sinAngle = Math.sin(textureAngle);
3134               g2D.setPaint(new TexturePaint(textureImage,
3135                   new Rectangle2D.Double(
3136                       floorTexture.getXOffset() * textureWidth * textureScale * cosAngle
3137                       - floorTexture.getYOffset() * textureHeight * textureScale * sinAngle,
3138                       - floorTexture.getXOffset() * textureWidth * textureScale * sinAngle
3139                       - floorTexture.getYOffset() * textureHeight * textureScale * cosAngle,
3140                       textureWidth * textureScale, textureHeight * textureScale)));
3141             }
3142           }
3143         }
3144 
3145         Composite oldComposite = setTransparency(g2D, 0.75f);
3146         // Rotate graphics to rotate texture with requested angle
3147         // and draw shape rotated with the opposite angle
3148         g2D.rotate(textureAngle, 0, 0);
3149         AffineTransform rotation = textureAngle != 0
3150             ? AffineTransform.getRotateInstance(-textureAngle, 0, 0)
3151             : null;
3152         Shape roomShape = ShapeTools.getShape(room.getPoints(), true, rotation);
3153         fillShape(g2D, roomShape, paintMode);
3154         g2D.setComposite(oldComposite);
3155 
3156         g2D.setPaint(foregroundColor);
3157         g2D.draw(roomShape);
3158         g2D.rotate(-textureAngle, 0, 0);
3159       }
3160     }
3161   }
3162 
3163   /**
3164    * Fills the given <code>shape</code>.
3165    */
fillShape(Graphics2D g2D, Shape shape, PaintMode paintMode)3166   private void fillShape(Graphics2D g2D, Shape shape, PaintMode paintMode) {
3167     if (paintMode == PaintMode.PRINT
3168         && g2D.getPaint() instanceof TexturePaint
3169         && OperatingSystem.isMacOSX()
3170         && OperatingSystem.isJavaVersionBetween("1.7", "1.8.0_152")) {
3171       Shape clip = g2D.getClip();
3172       g2D.setClip(shape);
3173       TexturePaint paint = (TexturePaint)g2D.getPaint();
3174       BufferedImage image = paint.getImage();
3175       Rectangle2D anchorRect = paint.getAnchorRect();
3176       Rectangle2D shapeBounds = shape.getBounds2D();
3177       double firstX = anchorRect.getX() + Math.round(shapeBounds.getX() / anchorRect.getWidth()) * anchorRect.getWidth();
3178       if (firstX > shapeBounds.getX()) {
3179         firstX -= anchorRect.getWidth();
3180       }
3181       double firstY = anchorRect.getY() + Math.round(shapeBounds.getY() / anchorRect.getHeight()) * anchorRect.getHeight();
3182       if (firstY > shapeBounds.getY()) {
3183         firstY -= anchorRect.getHeight();
3184       }
3185       for (double x = firstX;
3186           x < shapeBounds.getMaxX(); x += anchorRect.getWidth()) {
3187         for (double y = firstY; y < shapeBounds.getMaxY(); y += anchorRect.getHeight()) {
3188           AffineTransform transform = AffineTransform.getTranslateInstance(x, y);
3189           transform.concatenate(AffineTransform.getScaleInstance(
3190               anchorRect.getWidth() / image.getWidth(), anchorRect.getHeight() / image.getHeight()));
3191           g2D.drawRenderedImage(image, transform);
3192         }
3193       }
3194       g2D.setClip(clip);
3195     } else {
3196       g2D.fill(shape);
3197     }
3198   }
3199 
3200   /**
3201    * Returns <code>true</code> if <code>TextureManager</code> can be used to manage textures.
3202    */
isTextureManagerAvailable()3203   private static boolean isTextureManagerAvailable() {
3204     try {
3205       return !Boolean.getBoolean("com.eteks.sweethome3d.no3D")
3206           // Refuse to share textures under Mac OS X with Java 1.7 for performance reasons
3207           && !(OperatingSystem.isMacOSX()
3208                && OperatingSystem.isJavaVersionGreaterOrEqual("1.7"));
3209     } catch (AccessControlException ex) {
3210       // If com.eteks.sweethome3d.no3D can't be read,
3211       // security manager won't allow to access to Java 3D DLLs required by TextureManager class too
3212     }
3213     return false;
3214   }
3215 
3216   /**
3217    * Paints rooms name and area.
3218    */
paintRoomsNameAndArea(Graphics2D g2D, List<Selectable> selectedItems, float planScale, Color foregroundColor, PaintMode paintMode)3219   private void paintRoomsNameAndArea(Graphics2D g2D, List<Selectable> selectedItems, float planScale,
3220                                      Color foregroundColor, PaintMode paintMode) {
3221     g2D.setPaint(foregroundColor);
3222     Font previousFont = g2D.getFont();
3223     for (Room room : this.sortedLevelRooms) {
3224       boolean selectedRoom = selectedItems.contains(room);
3225       // In clipboard paint mode, paint room only if it is selected
3226       if (paintMode != PaintMode.CLIPBOARD
3227           || selectedRoom) {
3228         float xRoomCenter = room.getXCenter();
3229         float yRoomCenter = room.getYCenter();
3230         String name = room.getName();
3231         if (name != null) {
3232           name = name.trim();
3233           if (name.length() > 0) {
3234             paintText(g2D, room.getClass(), name, room.getNameStyle(), null,
3235                 xRoomCenter + room.getNameXOffset(),
3236                 yRoomCenter + room.getNameYOffset(),
3237                 room.getNameAngle(), previousFont);
3238           }
3239         }
3240         if (room.isAreaVisible()) {
3241           float area = room.getArea();
3242           if (area > 0.01f) {
3243             // Draw room area
3244             String areaText = this.preferences.getLengthUnit().getAreaFormatWithUnit().format(area);
3245             paintText(g2D, room.getClass(), areaText, room.getAreaStyle(), null,
3246                 xRoomCenter + room.getAreaXOffset(),
3247                 yRoomCenter + room.getAreaYOffset(),
3248                 room.getAreaAngle(), previousFont);
3249           }
3250         }
3251       }
3252     }
3253     g2D.setFont(previousFont);
3254   }
3255 
3256   /**
3257    * Paints the given <code>text</code> centered at the point (<code>x</code>,<code>y</code>).
3258    */
paintText(Graphics2D g2D, Class<? extends Selectable> selectableClass, String text, TextStyle style, Integer outlineColor, float x, float y, float angle, Font defaultFont)3259   private void paintText(Graphics2D g2D,
3260                          Class<? extends Selectable> selectableClass,
3261                          String text, TextStyle style, Integer outlineColor,
3262                          float x, float y, float angle,
3263                          Font defaultFont) {
3264     AffineTransform previousTransform = g2D.getTransform();
3265     g2D.translate(x, y);
3266     g2D.rotate(angle);
3267     if (style == null) {
3268       style = this.preferences.getDefaultTextStyle(selectableClass);
3269     }
3270     FontMetrics fontMetrics = getFontMetrics(defaultFont, style);
3271     String [] lines = text.split("\n");
3272     float [] lineWidths = new float [lines.length];
3273     float textWidth = -Float.MAX_VALUE;
3274     for (int i = 0; i < lines.length; i++) {
3275       lineWidths [i] = (float)fontMetrics.getStringBounds(lines [i], g2D).getWidth();
3276       textWidth = Math.max(lineWidths [i], textWidth);
3277     }
3278     BasicStroke stroke = null;
3279     Font font;
3280     if (outlineColor != null) {
3281       stroke = new BasicStroke(style.getFontSize() * 0.05f);
3282       TextStyle outlineStyle = style.deriveStyle(style.getFontSize() - stroke.getLineWidth());
3283       font = getFont(defaultFont, outlineStyle);
3284       g2D.setStroke(stroke);
3285     } else {
3286       font = getFont(defaultFont, style);
3287     }
3288     g2D.setFont(font);
3289 
3290     for (int i = lines.length - 1; i >= 0; i--) {
3291       String line = lines [i];
3292       float translationX;
3293       if (style.getAlignment() == TextStyle.Alignment.LEFT) {
3294         translationX = 0;
3295       } else if (style.getAlignment() == TextStyle.Alignment.RIGHT) {
3296         translationX = -lineWidths [i];
3297       } else { // CENTER
3298         translationX = -lineWidths [i] / 2;
3299       }
3300       if (outlineColor != null) {
3301         translationX += stroke.getLineWidth() / 2;
3302       }
3303       g2D.translate(translationX, 0);
3304       if (outlineColor != null) {
3305         // Draw text outline
3306         Color defaultColor = g2D.getColor();
3307         g2D.setColor(new Color(outlineColor));
3308         TextLayout textLayout = new TextLayout(line, font, g2D.getFontRenderContext());
3309         g2D.draw(textLayout.getOutline(null));
3310         g2D.setColor(defaultColor);
3311       }
3312       // Draw text
3313       g2D.drawString(line, 0, 0);
3314       g2D.translate(-translationX, -fontMetrics.getHeight());
3315     }
3316     g2D.setTransform(previousTransform);
3317   }
3318 
3319   /**
3320    * Paints the outline of rooms among <code>items</code> and indicators if
3321    * <code>items</code> contains only one room and indicator paint isn't <code>null</code>.
3322    */
paintRoomsOutline(Graphics2D g2D, List<Selectable> items, Paint selectionOutlinePaint, Stroke selectionOutlineStroke, Paint indicatorPaint, float planScale, Color foregroundColor)3323   private void paintRoomsOutline(Graphics2D g2D, List<Selectable> items,
3324                           Paint selectionOutlinePaint, Stroke selectionOutlineStroke,
3325                           Paint indicatorPaint, float planScale, Color foregroundColor) {
3326     Collection<Room> rooms = Home.getRoomsSubList(items);
3327     AffineTransform previousTransform = g2D.getTransform();
3328     float scaleInverse = 1 / planScale;
3329     // Draw selection border
3330     for (Room room : rooms) {
3331       if (isViewableAtSelectedLevel(room)) {
3332         g2D.setPaint(selectionOutlinePaint);
3333         g2D.setStroke(selectionOutlineStroke);
3334         g2D.draw(ShapeTools.getShape(room.getPoints(), true, null));
3335 
3336         if (indicatorPaint != null) {
3337           g2D.setPaint(indicatorPaint);
3338           // Draw points of the room
3339           for (float [] point : room.getPoints()) {
3340             g2D.translate(point [0], point [1]);
3341             g2D.scale(scaleInverse, scaleInverse);
3342             g2D.setStroke(POINT_STROKE);
3343             g2D.fill(WALL_POINT);
3344             g2D.setTransform(previousTransform);
3345           }
3346         }
3347       }
3348     }
3349 
3350     // Draw rooms area
3351     g2D.setPaint(foregroundColor);
3352     g2D.setStroke(new BasicStroke(getStrokeWidth(Room.class, PaintMode.PAINT) / planScale));
3353     for (Room room : rooms) {
3354       if (isViewableAtSelectedLevel(room)) {
3355         g2D.draw(ShapeTools.getShape(room.getPoints(), true, null));
3356       }
3357     }
3358 
3359     // Paint resize indicators of the room if indicator paint exists
3360     if (items.size() == 1
3361         && rooms.size() == 1
3362         && indicatorPaint != null) {
3363       Room selectedRoom = rooms.iterator().next();
3364       if (isViewableAtSelectedLevel(selectedRoom)) {
3365         g2D.setPaint(indicatorPaint);
3366         paintPointsResizeIndicators(g2D, selectedRoom, indicatorPaint, planScale, true, 0, 0, true);
3367         paintRoomNameOffsetIndicator(g2D, selectedRoom, indicatorPaint, planScale);
3368         paintRoomAreaOffsetIndicator(g2D, selectedRoom, indicatorPaint, planScale);
3369       }
3370     }
3371   }
3372 
3373   /**
3374    * Paints resize indicators on selectable <code>item</code>.
3375    */
paintPointsResizeIndicators(Graphics2D g2D, Selectable item, Paint indicatorPaint, float planScale, boolean closedPath, float angleAtStart, float angleAtEnd, boolean orientateIndicatorOutsideShape)3376   private void paintPointsResizeIndicators(Graphics2D g2D, Selectable item,
3377                                            Paint indicatorPaint,
3378                                            float planScale,
3379                                            boolean closedPath,
3380                                            float   angleAtStart,
3381                                            float   angleAtEnd,
3382                                            boolean orientateIndicatorOutsideShape) {
3383     if (this.resizeIndicatorVisible) {
3384       g2D.setPaint(indicatorPaint);
3385       g2D.setStroke(INDICATOR_STROKE);
3386       AffineTransform previousTransform = g2D.getTransform();
3387       float scaleInverse = 1 / planScale;
3388       float [][] points = item.getPoints();
3389       Shape resizeIndicator = getIndicator(item, IndicatorType.RESIZE);
3390       for (int i = 0; i < points.length; i++) {
3391         // Draw resize indicator at point
3392         float [] point = points [i];
3393         g2D.translate(point[0], point[1]);
3394         g2D.scale(scaleInverse, scaleInverse);
3395         float [] previousPoint = i == 0
3396             ? points [points.length - 1]
3397             : points [i -1];
3398         float [] nextPoint = i == points.length - 1
3399             ? points [0]
3400             : points [i + 1];
3401         double angle;
3402         if (closedPath || (i > 0 && i < points.length - 1)) {
3403           // Compute the angle of the mean normalized normal at point i
3404           float distance1 = (float)Point2D.distance(
3405               previousPoint [0], previousPoint [1], point [0], point [1]);
3406           float xNormal1 = (point [1] - previousPoint [1]) / distance1;
3407           float yNormal1 = (previousPoint [0] - point [0]) / distance1;
3408           float distance2 = (float)Point2D.distance(
3409               nextPoint [0], nextPoint [1], point [0], point [1]);
3410           float xNormal2 = (nextPoint [1] - point [1]) / distance2;
3411           float yNormal2 = (point [0] - nextPoint [0]) / distance2;
3412           angle = Math.atan2(yNormal1 + yNormal2, xNormal1 + xNormal2);
3413           // Ensure the indicator will be drawn outside of shape
3414           if (orientateIndicatorOutsideShape
3415                 && item.containsPoint(point [0] + (float)Math.cos(angle),
3416                       point [1] + (float)Math.sin(angle), 0.001f)
3417               || !orientateIndicatorOutsideShape
3418                   && (xNormal1 * yNormal2 - yNormal1 * xNormal2) < 0) {
3419             angle += Math.PI;
3420           }
3421         } else if (i == 0) {
3422           angle = angleAtStart;
3423         } else {
3424           angle = angleAtEnd;
3425         }
3426         g2D.rotate(angle);
3427         g2D.draw(resizeIndicator);
3428         g2D.setTransform(previousTransform);
3429       }
3430     }
3431   }
3432 
3433   /**
3434    * Returns the shape of the given indicator type.
3435    */
getIndicator(Selectable item, IndicatorType indicatorType)3436   protected Shape getIndicator(Selectable item, IndicatorType indicatorType) {
3437     if (IndicatorType.RESIZE.equals(indicatorType)) {
3438       if (item instanceof HomePieceOfFurniture) {
3439         return FURNITURE_RESIZE_INDICATOR;
3440       } else if (item instanceof Compass) {
3441         return COMPASS_RESIZE_INDICATOR;
3442       } else {
3443         return WALL_AND_LINE_RESIZE_INDICATOR;
3444       }
3445     } else if (IndicatorType.ROTATE.equals(indicatorType)) {
3446       if (item instanceof HomePieceOfFurniture) {
3447         return FURNITURE_ROTATION_INDICATOR;
3448       } else if (item instanceof Compass) {
3449         return COMPASS_ROTATION_INDICATOR;
3450       } else if (item instanceof Camera) {
3451         return CAMERA_YAW_ROTATION_INDICATOR;
3452       }
3453     } else if (IndicatorType.ELEVATE.equals(indicatorType)) {
3454       if (item instanceof Camera) {
3455         return CAMERA_ELEVATION_INDICATOR;
3456       } else {
3457         return ELEVATION_INDICATOR;
3458       }
3459     } else if (IndicatorType.RESIZE_HEIGHT.equals(indicatorType)) {
3460       if (item instanceof HomePieceOfFurniture) {
3461         return FURNITURE_HEIGHT_INDICATOR;
3462       }
3463     } else if (IndicatorType.CHANGE_POWER.equals(indicatorType)) {
3464       if (item instanceof HomeLight) {
3465         return LIGHT_POWER_INDICATOR;
3466       }
3467     } else if (IndicatorType.MOVE_TEXT.equals(indicatorType)) {
3468       return TEXT_LOCATION_INDICATOR;
3469     } else if (IndicatorType.ROTATE_TEXT.equals(indicatorType)) {
3470       return TEXT_ANGLE_INDICATOR;
3471     } else if (IndicatorType.ROTATE_PITCH.equals(indicatorType)) {
3472       if (item instanceof HomePieceOfFurniture) {
3473         return FURNITURE_PITCH_ROTATION_INDICATOR;
3474       } else if (item instanceof Camera) {
3475         return CAMERA_PITCH_ROTATION_INDICATOR;
3476       }
3477     } else if (IndicatorType.ROTATE_ROLL.equals(indicatorType)) {
3478       if (item instanceof HomePieceOfFurniture) {
3479         return FURNITURE_ROLL_ROTATION_INDICATOR;
3480       }
3481     } else if (IndicatorType.ARC_EXTENT.equals(indicatorType)) {
3482       if (item instanceof Wall) {
3483         return WALL_ARC_EXTENT_INDICATOR;
3484       }
3485     }
3486     return null;
3487   }
3488 
3489   /**
3490    * Paints name indicator on <code>room</code>.
3491    */
paintRoomNameOffsetIndicator(Graphics2D g2D, Room room, Paint indicatorPaint, float planScale)3492   private void paintRoomNameOffsetIndicator(Graphics2D g2D, Room room,
3493                                             Paint indicatorPaint,
3494                                             float planScale) {
3495     if (this.resizeIndicatorVisible
3496         && room.getName() != null
3497         && room.getName().trim().length() > 0) {
3498       float xName = room.getXCenter() + room.getNameXOffset();
3499       float yName = room.getYCenter() + room.getNameYOffset();
3500       paintTextIndicators(g2D, room.getClass(), getLineCount(room.getName()),
3501           room.getNameStyle(), xName, yName, room.getNameAngle(), indicatorPaint, planScale);
3502     }
3503   }
3504 
3505   /**
3506    * Paints resize indicator on <code>room</code>.
3507    */
paintRoomAreaOffsetIndicator(Graphics2D g2D, Room room, Paint indicatorPaint, float planScale)3508   private void paintRoomAreaOffsetIndicator(Graphics2D g2D, Room room,
3509                                             Paint indicatorPaint,
3510                                             float planScale) {
3511     if (this.resizeIndicatorVisible
3512         && room.isAreaVisible()
3513         && room.getArea() > 0.01f) {
3514       float xArea = room.getXCenter() + room.getAreaXOffset();
3515       float yArea = room.getYCenter() + room.getAreaYOffset();
3516       paintTextIndicators(g2D, room.getClass(), 1, room.getAreaStyle(), xArea, yArea, room.getAreaAngle(),
3517           indicatorPaint, planScale);
3518     }
3519   }
3520 
3521   /**
3522    * Paints text location and angle indicators at the given coordinates.
3523    */
paintTextIndicators(Graphics2D g2D, Class<? extends Selectable> selectableClass, int lineCount, TextStyle style, float x, float y, float angle, Paint indicatorPaint, float planScale)3524   private void paintTextIndicators(Graphics2D g2D,
3525                                    Class<? extends Selectable> selectableClass,
3526                                    int lineCount, TextStyle style,
3527                                    float x, float y, float angle,
3528                                    Paint indicatorPaint,
3529                                    float planScale) {
3530     if (this.resizeIndicatorVisible) {
3531       g2D.setPaint(indicatorPaint);
3532       g2D.setStroke(INDICATOR_STROKE);
3533       AffineTransform previousTransform = g2D.getTransform();
3534       float scaleInverse = 1 / planScale;
3535       g2D.translate(x, y);
3536       g2D.rotate(angle);
3537       g2D.scale(scaleInverse, scaleInverse);
3538       if (Label.class.isAssignableFrom(selectableClass)) {
3539         g2D.draw(LABEL_CENTER_INDICATOR);
3540       } else {
3541         g2D.draw(getIndicator(null, IndicatorType.MOVE_TEXT));
3542       }
3543       if (style == null) {
3544         style = this.preferences.getDefaultTextStyle(selectableClass);
3545       }
3546       FontMetrics fontMetrics = getFontMetrics(g2D.getFont(), style);
3547       g2D.setTransform(previousTransform);
3548       g2D.translate(x, y);
3549       g2D.rotate(angle);
3550       g2D.translate(0, -fontMetrics.getHeight() * (lineCount - 1)
3551           - fontMetrics.getAscent() * (Label.class.isAssignableFrom(selectableClass) ? 1 : 0.85));
3552       g2D.scale(scaleInverse, scaleInverse);
3553       g2D.draw(getIndicator(null, IndicatorType.ROTATE_TEXT));
3554       g2D.setTransform(previousTransform);
3555     }
3556   }
3557 
3558   /**
3559    * Returns the number of lines in the given <code>text</code> ignoring trailing line returns.
3560    */
getLineCount(String text)3561   private int getLineCount(String text) {
3562     int lineCount = 1;
3563     int i = text.length() - 1;
3564     while (i >= 0 && text.charAt(i) == '\n') {
3565       i--;
3566     }
3567     for ( ; i >= 0; i--) {
3568       if (text.charAt(i) == '\n') {
3569         lineCount++;
3570       }
3571     }
3572     return lineCount;
3573   }
3574 
3575   /**
3576    * Paints walls.
3577    */
paintWalls(Graphics2D g2D, List<Selectable> selectedItems, float planScale, Color backgroundColor, Color foregroundColor, PaintMode paintMode)3578   private void paintWalls(Graphics2D g2D, List<Selectable> selectedItems, float planScale,
3579                           Color backgroundColor, Color foregroundColor, PaintMode paintMode) {
3580     Collection<Wall> paintedWalls;
3581     Map<Collection<Wall>, Area> wallAreas;
3582     if (paintMode != PaintMode.CLIPBOARD) {
3583       wallAreas = getWallAreas();
3584     } else {
3585       // In clipboard paint mode, paint only selected walls
3586       paintedWalls = Home.getWallsSubList(selectedItems);
3587       wallAreas = getWallAreas(getDrawableWallsInSelectedLevel(paintedWalls));
3588     }
3589     float wallPaintScale = paintMode == PaintMode.PRINT
3590         ? planScale / 72 * 150 // Adjust scale to 150 dpi for print
3591         : planScale;
3592     Composite oldComposite = null;
3593     if (paintMode == PaintMode.PAINT
3594         && this.backgroundPainted
3595         && this.backgroundImageCache != null
3596         && this.wallsDoorsOrWindowsModification) {
3597       // Paint walls with half transparent paint when a wall or a door/window in the base plan is being handled
3598       oldComposite = setTransparency(g2D, 0.5f);
3599     }
3600     for (Map.Entry<Collection<Wall>, Area> areaEntry : wallAreas.entrySet()) {
3601       TextureImage wallPattern = areaEntry.getKey().iterator().next().getPattern();
3602       fillAndDrawWallsArea(g2D, areaEntry.getValue(), planScale,
3603           getWallPaint(wallPaintScale, backgroundColor, foregroundColor,
3604               wallPattern != null ? wallPattern : this.preferences.getWallPattern()), foregroundColor, paintMode);
3605     }
3606     if (oldComposite != null) {
3607       g2D.setComposite(oldComposite);
3608     }
3609   }
3610 
3611   /**
3612    * Fills and paints the given area.
3613    */
fillAndDrawWallsArea(Graphics2D g2D, Area area, float planScale, Paint fillPaint, Paint drawPaint, PaintMode paintMode)3614   private void fillAndDrawWallsArea(Graphics2D g2D, Area area, float planScale, Paint fillPaint,
3615                                     Paint drawPaint, PaintMode paintMode) {
3616     // Fill walls area
3617     g2D.setPaint(fillPaint);
3618     fillShape(g2D, area, paintMode);
3619     // Draw walls area
3620     g2D.setPaint(drawPaint);
3621     g2D.setStroke(new BasicStroke(getStrokeWidth(Wall.class, paintMode) / planScale));
3622     g2D.draw(area);
3623   }
3624 
3625   /**
3626    * Paints the outline of walls among <code>items</code> and a resize indicator if
3627    * <code>items</code> contains only one wall and indicator paint isn't <code>null</code>.
3628    */
paintWallsOutline(Graphics2D g2D, List<Selectable> items, Paint selectionOutlinePaint, Stroke selectionOutlineStroke, Paint indicatorPaint, float planScale, Color foregroundColor)3629   private void paintWallsOutline(Graphics2D g2D, List<Selectable> items,
3630                                  Paint selectionOutlinePaint, Stroke selectionOutlineStroke,
3631                                  Paint indicatorPaint, float planScale, Color foregroundColor) {
3632     float scaleInverse = 1 / planScale;
3633     Collection<Wall> walls = Home.getWallsSubList(items);
3634     AffineTransform previousTransform = g2D.getTransform();
3635     for (Wall wall : walls) {
3636       if (isViewableAtSelectedLevel(wall)) {
3637         // Draw selection border
3638         g2D.setPaint(selectionOutlinePaint);
3639         g2D.setStroke(selectionOutlineStroke);
3640         g2D.draw(ShapeTools.getShape(wall.getPoints(), true, null));
3641 
3642         if (indicatorPaint != null) {
3643           // Draw start point of the wall
3644           g2D.translate(wall.getXStart(), wall.getYStart());
3645           g2D.scale(scaleInverse, scaleInverse);
3646           g2D.setPaint(indicatorPaint);
3647           g2D.setStroke(POINT_STROKE);
3648           g2D.fill(WALL_POINT);
3649 
3650           Float arcExtent = wall.getArcExtent();
3651           double indicatorAngle;
3652           double distanceAtScale;
3653           float xArcCircleCenter = 0;
3654           float yArcCircleCenter = 0;
3655           double arcCircleRadius = 0;
3656           double startPointToEndPointDistance = wall.getStartPointToEndPointDistance();
3657           double wallAngle = Math.atan2(wall.getYEnd() - wall.getYStart(),
3658               wall.getXEnd() - wall.getXStart());
3659           if (arcExtent != null
3660               && arcExtent.floatValue() != 0) {
3661             xArcCircleCenter = wall.getXArcCircleCenter();
3662             yArcCircleCenter = wall.getYArcCircleCenter();
3663             arcCircleRadius = Point2D.distance(wall.getXStart(), wall.getYStart(),
3664                 xArcCircleCenter, yArcCircleCenter);
3665             distanceAtScale = arcCircleRadius * Math.abs(arcExtent) * planScale;
3666             indicatorAngle = Math.atan2(yArcCircleCenter - wall.getYStart(),
3667                     xArcCircleCenter - wall.getXStart())
3668                 + (arcExtent > 0 ? -Math.PI / 2 : Math.PI /2);
3669           } else {
3670             distanceAtScale = startPointToEndPointDistance * planScale;
3671             indicatorAngle = wallAngle;
3672           }
3673           // If the distance between start and end points is < 30
3674           if (distanceAtScale < 30) {
3675             // Draw only one orientation indicator between the two points
3676             g2D.rotate(wallAngle);
3677             if (arcExtent != null) {
3678               double wallToStartPointArcCircleCenterAngle = Math.abs(arcExtent) > Math.PI
3679                   ? -(Math.PI + arcExtent) / 2
3680                   : (Math.PI - arcExtent) / 2;
3681               float arcCircleCenterToWallDistance = (float)(Math.tan(wallToStartPointArcCircleCenterAngle)
3682                   * startPointToEndPointDistance / 2);
3683               g2D.translate(startPointToEndPointDistance * planScale / 2,
3684                   (arcCircleCenterToWallDistance - arcCircleRadius * (Math.abs(wallAngle) > Math.PI / 2 ? -1: 1)) * planScale);
3685             } else {
3686               g2D.translate(distanceAtScale / 2, 0);
3687             }
3688           } else {
3689             // Draw orientation indicator at start of the wall
3690             g2D.rotate(indicatorAngle);
3691             g2D.translate(8, 0);
3692           }
3693           g2D.draw(WALL_ORIENTATION_INDICATOR);
3694           g2D.setTransform(previousTransform);
3695 
3696           // Draw end point of the wall
3697           g2D.translate(wall.getXEnd(), wall.getYEnd());
3698           g2D.scale(scaleInverse, scaleInverse);
3699           g2D.fill(WALL_POINT);
3700           if (distanceAtScale >= 30) {
3701             if (arcExtent != null) {
3702               indicatorAngle += arcExtent;
3703             }
3704             // Draw orientation indicator at end of the wall
3705             g2D.rotate(indicatorAngle);
3706             g2D.translate(-10, 0);
3707             g2D.draw(WALL_ORIENTATION_INDICATOR);
3708           }
3709           g2D.setTransform(previousTransform);
3710         }
3711       }
3712     }
3713     // Draw walls area
3714     g2D.setPaint(foregroundColor);
3715     g2D.setStroke(new BasicStroke(getStrokeWidth(Wall.class, PaintMode.PAINT) / planScale));
3716     for (Area area : getWallAreas(getDrawableWallsInSelectedLevel(walls)).values()) {
3717       g2D.draw(area);
3718     }
3719 
3720     // Paint resize indicator of the wall if indicator paint exists
3721     if (items.size() == 1
3722         && walls.size() == 1
3723         && indicatorPaint != null) {
3724       Wall wall = walls.iterator().next();
3725       if (isViewableAtSelectedLevel(wall)) {
3726         paintWallResizeIndicators(g2D, wall, indicatorPaint, planScale);
3727       }
3728     }
3729   }
3730 
3731   /**
3732    * Returns <code>true</code> if the given item can be viewed in the plan at the selected level.
3733    */
isViewableAtSelectedLevel(Elevatable item)3734   protected boolean isViewableAtSelectedLevel(Elevatable item) {
3735     Level level = item.getLevel();
3736     return level == null
3737         || (level.isViewable()
3738             && item.isAtLevel(this.home.getSelectedLevel()));
3739   }
3740 
3741   /**
3742    * Paints resize indicators on <code>wall</code>.
3743    */
paintWallResizeIndicators(Graphics2D g2D, Wall wall, Paint indicatorPaint, float planScale)3744   private void paintWallResizeIndicators(Graphics2D g2D, Wall wall,
3745                                          Paint indicatorPaint,
3746                                          float planScale) {
3747     if (this.resizeIndicatorVisible) {
3748       g2D.setPaint(indicatorPaint);
3749       g2D.setStroke(INDICATOR_STROKE);
3750 
3751       AffineTransform previousTransform = g2D.getTransform();
3752       float scaleInverse = 1 / planScale;
3753       float [][] wallPoints = wall.getPoints();
3754       int leftSideMiddlePointIndex = wallPoints.length / 4;
3755       double wallAngle = Math.atan2(wall.getYEnd() - wall.getYStart(),
3756           wall.getXEnd() - wall.getXStart());
3757 
3758       // Draw arc extent indicator at wall middle
3759       if (wallPoints.length % 4 == 0) {
3760         g2D.translate((wallPoints [leftSideMiddlePointIndex - 1][0] + wallPoints [leftSideMiddlePointIndex][0]) / 2,
3761             (wallPoints [leftSideMiddlePointIndex - 1][1] + wallPoints [leftSideMiddlePointIndex][1]) / 2);
3762       } else {
3763         g2D.translate(wallPoints [leftSideMiddlePointIndex][0], wallPoints [leftSideMiddlePointIndex][1]);
3764       }
3765       g2D.scale(scaleInverse, scaleInverse);
3766       g2D.rotate(wallAngle + Math.PI);
3767       g2D.draw(getIndicator(wall, IndicatorType.ARC_EXTENT));
3768       g2D.setTransform(previousTransform);
3769 
3770       Float arcExtent = wall.getArcExtent();
3771       double indicatorAngle;
3772       if (arcExtent != null
3773           && arcExtent.floatValue() != 0) {
3774         indicatorAngle = Math.atan2(wall.getYArcCircleCenter() - wall.getYEnd(),
3775             wall.getXArcCircleCenter() - wall.getXEnd())
3776             + (arcExtent > 0 ? -Math.PI / 2 : Math.PI /2);
3777       } else {
3778         indicatorAngle = wallAngle;
3779       }
3780 
3781       // Draw resize indicator at wall end point
3782       g2D.translate(wall.getXEnd(), wall.getYEnd());
3783       g2D.scale(scaleInverse, scaleInverse);
3784       g2D.rotate(indicatorAngle);
3785       g2D.draw(getIndicator(wall, IndicatorType.RESIZE));
3786       g2D.setTransform(previousTransform);
3787 
3788       if (arcExtent != null) {
3789         indicatorAngle += Math.PI - arcExtent;
3790       } else {
3791         indicatorAngle += Math.PI;
3792       }
3793 
3794       // Draw resize indicator at wall start point
3795       g2D.translate(wall.getXStart(), wall.getYStart());
3796       g2D.scale(scaleInverse, scaleInverse);
3797       g2D.rotate(indicatorAngle);
3798       g2D.draw(getIndicator(wall, IndicatorType.RESIZE));
3799       g2D.setTransform(previousTransform);
3800     }
3801   }
3802 
3803   /**
3804    * Returns areas matching the union of home wall shapes sorted by pattern.
3805    */
getWallAreas()3806   private Map<Collection<Wall>, Area> getWallAreas() {
3807     if (this.wallAreasCache == null) {
3808       this.wallAreasCache = getWallAreas(getDrawableWallsInSelectedLevel(this.home.getWalls()));
3809     }
3810     return this.wallAreasCache;
3811   }
3812 
3813   /**
3814    * Returns the walls that belong to the selected level in home.
3815    */
getDrawableWallsInSelectedLevel(Collection<Wall> walls)3816   private Collection<Wall> getDrawableWallsInSelectedLevel(Collection<Wall> walls) {
3817     List<Wall> wallsInSelectedLevel = new ArrayList<Wall>();
3818     for (Wall wall : walls) {
3819       if (isViewableAtSelectedLevel(wall)) {
3820         wallsInSelectedLevel.add(wall);
3821       }
3822     }
3823     return wallsInSelectedLevel;
3824   }
3825 
3826   /**
3827    * Returns areas matching the union of <code>walls</code> shapes sorted by pattern.
3828    */
getWallAreas(Collection<Wall> walls)3829   private Map<Collection<Wall>, Area> getWallAreas(Collection<Wall> walls) {
3830     if (walls.size() == 0) {
3831       return Collections.emptyMap();
3832     }
3833     // Check if all walls use the same pattern
3834     TextureImage pattern = walls.iterator().next().getPattern();
3835     boolean samePattern = true;
3836     for (Wall wall : walls) {
3837       if (pattern != wall.getPattern()) {
3838         samePattern = false;
3839         break;
3840       }
3841     }
3842     Map<Collection<Wall>, Area> wallAreas = new LinkedHashMap<Collection<Wall>, Area>();
3843     if (samePattern) {
3844       wallAreas.put(walls, getItemsArea(walls));
3845     } else {
3846       // Create walls sublists by pattern
3847       Map<TextureImage, Collection<Wall>> sortedWalls = new LinkedHashMap<TextureImage, Collection<Wall>>();
3848       for (Wall wall : walls) {
3849         TextureImage wallPattern = wall.getPattern();
3850         if (wallPattern == null) {
3851           wallPattern = this.preferences.getWallPattern();
3852         }
3853         Collection<Wall> patternWalls = sortedWalls.get(wallPattern);
3854         if (patternWalls == null) {
3855           patternWalls = new ArrayList<Wall>();
3856           sortedWalls.put(wallPattern, patternWalls);
3857         }
3858         patternWalls.add(wall);
3859       }
3860       for (Collection<Wall> patternWalls : sortedWalls.values()) {
3861         wallAreas.put(patternWalls, getItemsArea(patternWalls));
3862       }
3863     }
3864     return wallAreas;
3865   }
3866 
3867   /**
3868    * Returns an area matching the union of all <code>items</code> shapes.
3869    */
getItemsArea(Collection<? extends Selectable> items)3870   private Area getItemsArea(Collection<? extends Selectable> items) {
3871     Area itemsArea = new Area();
3872     for (Selectable item : items) {
3873       itemsArea.add(new Area(ShapeTools.getShape(item.getPoints(), true, null)));
3874     }
3875     return itemsArea;
3876   }
3877 
3878   /**
3879    * Returns the <code>Paint</code> object used to fill walls.
3880    */
getWallPaint(float planScale, Color backgroundColor, Color foregroundColor, TextureImage wallPattern)3881   private Paint getWallPaint(float planScale, Color backgroundColor, Color foregroundColor, TextureImage wallPattern) {
3882     BufferedImage patternImage = this.patternImagesCache.get(wallPattern);
3883     if (patternImage == null
3884         || !backgroundColor.equals(this.wallsPatternBackgroundCache)
3885         || !foregroundColor.equals(this.wallsPatternForegroundCache)) {
3886       patternImage = SwingTools.getPatternImage(wallPattern, backgroundColor, foregroundColor);
3887       this.patternImagesCache.put(wallPattern, patternImage);
3888       this.wallsPatternBackgroundCache = backgroundColor;
3889       this.wallsPatternForegroundCache = foregroundColor;
3890     }
3891     return new TexturePaint(patternImage,
3892         new Rectangle2D.Float(0, 0, 10 / planScale, 10 / planScale));
3893   }
3894 
3895   /**
3896    * Paints home furniture.
3897    */
paintFurniture(Graphics2D g2D, List<HomePieceOfFurniture> furniture, List<? extends Selectable> selectedItems, float planScale, Color backgroundColor, Color foregroundColor, Color furnitureOutlineColor, PaintMode paintMode, boolean paintIcon)3898   private void paintFurniture(Graphics2D g2D, List<HomePieceOfFurniture> furniture,
3899                               List<? extends Selectable> selectedItems, float planScale,
3900                               Color backgroundColor, Color foregroundColor,
3901                               Color furnitureOutlineColor,
3902                               PaintMode paintMode, boolean paintIcon) {
3903     if (!furniture.isEmpty()) {
3904       BasicStroke pieceBorderStroke = new BasicStroke(getStrokeWidth(HomePieceOfFurniture.class, paintMode) / planScale);
3905       Boolean allFurnitureViewedFromTop = null;
3906       // Draw furniture
3907       for (HomePieceOfFurniture piece : furniture) {
3908         if (piece.isVisible()) {
3909           boolean selectedPiece = selectedItems.contains(piece);
3910           if (piece instanceof HomeFurnitureGroup) {
3911             List<HomePieceOfFurniture> groupFurniture = ((HomeFurnitureGroup)piece).getFurniture();
3912             List<Selectable> emptyList = Collections.emptyList();
3913             paintFurniture(g2D, groupFurniture,
3914                 selectedPiece
3915                     ? groupFurniture
3916                     : emptyList,
3917                 planScale, backgroundColor, foregroundColor,
3918                 furnitureOutlineColor, paintMode, paintIcon);
3919           } else if (paintMode != PaintMode.CLIPBOARD
3920                     || selectedPiece) {
3921             // In clipboard paint mode, paint piece only if it is selected
3922             Shape pieceShape = ShapeTools.getShape(piece.getPoints(), true, null);
3923             Shape pieceShape2D;
3924             if (piece instanceof HomeDoorOrWindow) {
3925               HomeDoorOrWindow doorOrWindow = (HomeDoorOrWindow)piece;
3926               pieceShape2D = getDoorOrWindowWallPartShape(doorOrWindow);
3927               if (this.draggedItemsFeedback == null
3928                   || !this.draggedItemsFeedback.contains(piece)) {
3929                 paintDoorOrWindowWallThicknessArea(g2D, doorOrWindow, planScale, backgroundColor, foregroundColor, paintMode);
3930               }
3931               paintDoorOrWindowSashes(g2D, doorOrWindow, planScale, foregroundColor, paintMode);
3932             } else {
3933               pieceShape2D = pieceShape;
3934             }
3935 
3936             boolean viewedFromTop;
3937             if (this.preferences.isFurnitureViewedFromTop()) {
3938               if (piece.getPlanIcon() != null
3939                   || piece instanceof HomeDoorOrWindow) {
3940                 viewedFromTop = true;
3941               } else {
3942                 if (allFurnitureViewedFromTop == null) {
3943                   try {
3944                     // Evaluate allFurnitureViewedFromTop value as late as possible to avoid mandatory dependency towards Java 3D
3945                     allFurnitureViewedFromTop = !Boolean.getBoolean("com.eteks.sweethome3d.no3D")
3946                         && Component3DManager.getInstance().isOffScreenImageSupported();
3947                   } catch (AccessControlException ex) {
3948                     // If com.eteks.sweethome3d.no3D property can't be read,
3949                     // security manager won't allow to access to Java 3D DLLs required by PieceOfFurnitureModelIcon class too
3950                     allFurnitureViewedFromTop = false;
3951                   }
3952                 }
3953                 viewedFromTop = allFurnitureViewedFromTop.booleanValue();
3954               }
3955             } else {
3956               viewedFromTop = false;
3957             }
3958             if (paintIcon
3959                 && viewedFromTop) {
3960               if (piece instanceof HomeDoorOrWindow) {
3961                 // Draw doors and windows border
3962                 g2D.setPaint(backgroundColor);
3963                 g2D.fill(pieceShape2D);
3964                 g2D.setPaint(foregroundColor);
3965                 g2D.setStroke(pieceBorderStroke);
3966                 g2D.draw(pieceShape2D);
3967               } else {
3968                 paintPieceOfFurnitureTop(g2D, piece, pieceShape2D, pieceBorderStroke, planScale,
3969                     backgroundColor, foregroundColor, paintMode);
3970               }
3971               if (paintMode == PaintMode.PAINT) {
3972                 // Draw selection outline rectangle
3973                 g2D.setStroke(pieceBorderStroke);
3974                 g2D.setPaint(furnitureOutlineColor);
3975                 g2D.draw(pieceShape);
3976               }
3977             } else {
3978               if (paintIcon) {
3979                 // Draw its icon
3980                 paintPieceOfFurnitureIcon(g2D, piece, pieceShape2D, planScale,
3981                     backgroundColor, paintMode);
3982               }
3983               // Draw its border
3984               g2D.setPaint(foregroundColor);
3985               g2D.setStroke(pieceBorderStroke);
3986               g2D.draw(pieceShape2D);
3987               if (piece instanceof HomeDoorOrWindow
3988                   && paintMode == PaintMode.PAINT) {
3989                 // Draw outline rectangle
3990                 g2D.setPaint(furnitureOutlineColor);
3991                 g2D.draw(pieceShape);
3992               }
3993             }
3994           }
3995         }
3996       }
3997     }
3998   }
3999 
4000   /**
4001    * Returns the shape of the wall part of a door or a window.
4002    */
getDoorOrWindowWallPartShape(HomeDoorOrWindow doorOrWindow)4003   private Shape getDoorOrWindowWallPartShape(HomeDoorOrWindow doorOrWindow) {
4004     Rectangle2D doorOrWindowWallPartRectangle = getDoorOrWindowRectangle(doorOrWindow, true);
4005     // Apply rotation to the rectangle
4006     AffineTransform rotation = AffineTransform.getRotateInstance(
4007         doorOrWindow.getAngle(), doorOrWindow.getX(), doorOrWindow.getY());
4008     PathIterator it = doorOrWindowWallPartRectangle.getPathIterator(rotation);
4009     GeneralPath doorOrWindowWallPartShape = new GeneralPath();
4010     doorOrWindowWallPartShape.append(it, false);
4011     return doorOrWindowWallPartShape;
4012   }
4013 
4014   /**
4015    * Returns the rectangle of a door or a window.
4016    */
getDoorOrWindowRectangle(HomeDoorOrWindow doorOrWindow, boolean onlyWallPart)4017   private Rectangle2D getDoorOrWindowRectangle(HomeDoorOrWindow doorOrWindow, boolean onlyWallPart) {
4018     // Doors and windows can't be rotated along horizontal axes
4019     float wallThickness = doorOrWindow.getDepth() * (onlyWallPart ? doorOrWindow.getWallThickness() : 1);
4020     float wallDistance  = doorOrWindow.getDepth() * (onlyWallPart ? doorOrWindow.getWallDistance()  : 0);
4021     String cutOutShape = doorOrWindow.getCutOutShape();
4022     float width = doorOrWindow.getWidth();
4023     float wallWidth = doorOrWindow.getWallWidth() * width;
4024     float x = doorOrWindow.getX() - width / 2;
4025     x += doorOrWindow.isModelMirrored()
4026         ? (1 - doorOrWindow.getWallLeft() - doorOrWindow.getWallWidth()) * width
4027         : doorOrWindow.getWallLeft() * width;
4028     if (cutOutShape != null
4029         && !PieceOfFurniture.DEFAULT_CUT_OUT_SHAPE.equals(cutOutShape)) {
4030       // In case of a complex cut out, compute location and width of the window hole at wall intersection
4031       Shape shape = ShapeTools.getShape(cutOutShape);
4032       Rectangle2D bounds = shape.getBounds2D();
4033       if (doorOrWindow.isModelMirrored()) {
4034         x += (float)(1 - bounds.getX() - bounds.getWidth()) * wallWidth;
4035       } else {
4036         x += (float)bounds.getX() * wallWidth;
4037       }
4038       wallWidth *= bounds.getWidth();
4039     }
4040     Rectangle2D doorOrWindowWallPartRectangle = new Rectangle2D.Float(
4041         x, doorOrWindow.getY() - doorOrWindow.getDepth() / 2 + wallDistance,
4042         wallWidth, wallThickness);
4043     return doorOrWindowWallPartRectangle;
4044   }
4045 
4046   /**
4047    * Paints the shape of a door or a window in the thickness of the wall it intersects.
4048    */
paintDoorOrWindowWallThicknessArea(Graphics2D g2D, HomeDoorOrWindow doorOrWindow, float planScale, Color backgroundColor, Color foregroundColor, PaintMode paintMode)4049   private void paintDoorOrWindowWallThicknessArea(Graphics2D g2D, HomeDoorOrWindow doorOrWindow, float planScale,
4050                                                   Color backgroundColor, Color foregroundColor, PaintMode paintMode) {
4051     if (doorOrWindow.isWallCutOutOnBothSides()) {
4052       Area doorOrWindowWallArea = null;
4053       if (this.doorOrWindowWallThicknessAreasCache != null) {
4054         doorOrWindowWallArea = this.doorOrWindowWallThicknessAreasCache.get(doorOrWindow);
4055       }
4056 
4057       if (doorOrWindowWallArea == null) {
4058         Rectangle2D doorOrWindowRectangle = getDoorOrWindowRectangle(doorOrWindow, false);
4059         // Apply rotation to the rectangle
4060         AffineTransform rotation = AffineTransform.getRotateInstance(
4061             doorOrWindow.getAngle(), doorOrWindow.getX(), doorOrWindow.getY());
4062         PathIterator it = doorOrWindowRectangle.getPathIterator(rotation);
4063         GeneralPath doorOrWindowWallPartShape = new GeneralPath();
4064         doorOrWindowWallPartShape.append(it, false);
4065         Area doorOrWindowWallPartArea = new Area(doorOrWindowWallPartShape);
4066 
4067         doorOrWindowWallArea = new Area();
4068         for (Wall wall : home.getWalls()) {
4069           if (wall.isAtLevel(doorOrWindow.getLevel())
4070               && doorOrWindow.isParallelToWall(wall)) {
4071             Shape wallShape = ShapeTools.getShape(wall.getPoints(), true, null);
4072             Area wallArea = new Area(wallShape);
4073             wallArea.intersect(doorOrWindowWallPartArea);
4074             if (!wallArea.isEmpty()) {
4075               Rectangle2D doorOrWindowExtendedRectangle = new Rectangle2D.Float(
4076                   (float)doorOrWindowRectangle.getX(),
4077                   (float)doorOrWindowRectangle.getY() - 2 * wall.getThickness(),
4078                   (float)doorOrWindowRectangle.getWidth(),
4079                   (float)doorOrWindowRectangle.getWidth() + 4 * wall.getThickness());
4080               it = doorOrWindowExtendedRectangle.getPathIterator(rotation);
4081               GeneralPath path = new GeneralPath();
4082               path.append(it, false);
4083               wallArea = new Area(wallShape);
4084               wallArea.intersect(new Area(path));
4085               doorOrWindowWallArea.add(wallArea);
4086             }
4087           }
4088         }
4089       }
4090 
4091       if (this.doorOrWindowWallThicknessAreasCache == null) {
4092         this.doorOrWindowWallThicknessAreasCache = new WeakHashMap<HomeDoorOrWindow, Area>();
4093       }
4094       this.doorOrWindowWallThicknessAreasCache.put(doorOrWindow, doorOrWindowWallArea);
4095 
4096       g2D.setPaint(backgroundColor);
4097       g2D.fill(doorOrWindowWallArea);
4098       g2D.setPaint(foregroundColor);
4099       g2D.setStroke(new BasicStroke(getStrokeWidth(HomePieceOfFurniture.class, paintMode) / planScale));
4100       g2D.draw(doorOrWindowWallArea);
4101     }
4102   }
4103 
4104   /**
4105    * Paints the sashes of a door or a window.
4106    */
paintDoorOrWindowSashes(Graphics2D g2D, HomeDoorOrWindow doorOrWindow, float planScale, Color foregroundColor, PaintMode paintMode)4107   private void paintDoorOrWindowSashes(Graphics2D g2D, HomeDoorOrWindow doorOrWindow, float planScale,
4108                                        Color foregroundColor, PaintMode paintMode) {
4109     BasicStroke sashBorderStroke = new BasicStroke(getStrokeWidth(HomePieceOfFurniture.class, paintMode) / planScale);
4110     g2D.setPaint(foregroundColor);
4111     g2D.setStroke(sashBorderStroke);
4112     for (Sash sash : doorOrWindow.getSashes()) {
4113       g2D.draw(getDoorOrWindowSashShape(doorOrWindow, sash));
4114     }
4115   }
4116 
4117   /**
4118    * Returns the shape of a sash of a door or a window.
4119    */
getDoorOrWindowSashShape(HomeDoorOrWindow doorOrWindow, Sash sash)4120   private GeneralPath getDoorOrWindowSashShape(HomeDoorOrWindow doorOrWindow,
4121                                                Sash sash) {
4122     // Doors and windows can't be rotated along horizontal axes
4123     float modelMirroredSign = doorOrWindow.isModelMirrored() ? -1 : 1;
4124     float xAxis = modelMirroredSign * sash.getXAxis() * doorOrWindow.getWidth();
4125     float yAxis = sash.getYAxis() * doorOrWindow.getDepth();
4126     float sashWidth = sash.getWidth() * doorOrWindow.getWidth();
4127     float startAngle = (float)Math.toDegrees(sash.getStartAngle());
4128     if (doorOrWindow.isModelMirrored()) {
4129       startAngle = 180 - startAngle;
4130     }
4131     float extentAngle = modelMirroredSign * (float)Math.toDegrees(sash.getEndAngle() - sash.getStartAngle());
4132 
4133     Arc2D arc = new Arc2D.Float(xAxis - sashWidth, yAxis - sashWidth,
4134         2 * sashWidth, 2 * sashWidth,
4135         startAngle, extentAngle, Arc2D.PIE);
4136     AffineTransform transformation = AffineTransform.getTranslateInstance(doorOrWindow.getX(), doorOrWindow.getY());
4137     transformation.rotate(doorOrWindow.getAngle());
4138     transformation.translate(modelMirroredSign * -doorOrWindow.getWidth() / 2, -doorOrWindow.getDepth() / 2);
4139     PathIterator it = arc.getPathIterator(transformation);
4140     GeneralPath sashShape = new GeneralPath();
4141     sashShape.append(it, false);
4142     return sashShape;
4143   }
4144 
4145   /**
4146    * Paints home furniture visible name.
4147    */
paintFurnitureName(Graphics2D g2D, List<HomePieceOfFurniture> furniture, List<? extends Selectable> selectedItems, float planScale, Color foregroundColor, PaintMode paintMode)4148   private void paintFurnitureName(Graphics2D g2D, List<HomePieceOfFurniture> furniture,
4149                                   List<? extends Selectable> selectedItems, float planScale,
4150                                   Color foregroundColor, PaintMode paintMode) {
4151     Font previousFont = g2D.getFont();
4152     g2D.setPaint(foregroundColor);
4153     // Draw furniture name
4154     for (HomePieceOfFurniture piece : furniture) {
4155       if (piece.isVisible()) {
4156         boolean selectedPiece = selectedItems.contains(piece);
4157         if (piece instanceof HomeFurnitureGroup) {
4158           List<HomePieceOfFurniture> groupFurniture = ((HomeFurnitureGroup)piece).getFurniture();
4159           List<Selectable> emptyList = Collections.emptyList();
4160           paintFurnitureName(g2D, groupFurniture,
4161                selectedPiece
4162                    ? groupFurniture
4163                    : emptyList,
4164                planScale, foregroundColor, paintMode);
4165         }
4166         if (piece.isNameVisible()
4167             && (paintMode != PaintMode.CLIPBOARD
4168                 || selectedPiece)) {
4169           // In clipboard paint mode, paint piece only if it is selected
4170           String name = piece.getName().trim();
4171           if (name.length() > 0) {
4172             // Draw piece name
4173             paintText(g2D, piece.getClass(), name, piece.getNameStyle(), null,
4174                 piece.getX() + piece.getNameXOffset(),
4175                 piece.getY() + piece.getNameYOffset(),
4176                 piece.getNameAngle(), previousFont);
4177           }
4178         }
4179       }
4180     }
4181     g2D.setFont(previousFont);
4182   }
4183 
4184   /**
4185    * Paints the outline of furniture among <code>items</code> and indicators if
4186    * <code>items</code> contains only one piece and indicator paint isn't <code>null</code>.
4187    */
paintFurnitureOutline(Graphics2D g2D, List<Selectable> items, Paint selectionOutlinePaint, Stroke selectionOutlineStroke, Paint indicatorPaint, float planScale, Color foregroundColor)4188   private void paintFurnitureOutline(Graphics2D g2D, List<Selectable> items,
4189                                      Paint selectionOutlinePaint, Stroke selectionOutlineStroke,
4190                                      Paint indicatorPaint, float planScale,
4191                                      Color foregroundColor) {
4192     BasicStroke pieceBorderStroke = new BasicStroke(getStrokeWidth(HomePieceOfFurniture.class, PaintMode.PAINT) / planScale);
4193     BasicStroke pieceFrontBorderStroke = new BasicStroke(4 * getStrokeWidth(HomePieceOfFurniture.class, PaintMode.PAINT) / planScale,
4194         BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
4195 
4196     List<HomePieceOfFurniture> furniture = Home.getFurnitureSubList(items);
4197     Area furnitureGroupsArea = null;
4198     BasicStroke furnitureGroupsStroke = new BasicStroke(15 / planScale, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND);
4199     HomePieceOfFurniture lastGroup = null;
4200     Area furnitureInGroupsArea = null;
4201     List<HomePieceOfFurniture> homeFurniture = this.home.getFurniture();
4202     for (Iterator<HomePieceOfFurniture> it = furniture.iterator(); it.hasNext();) {
4203       HomePieceOfFurniture piece = it.next();
4204       if (piece.isVisible()
4205           && isViewableAtSelectedLevel(piece)) {
4206         HomePieceOfFurniture homePieceOfFurniture = getPieceOfFurnitureInHomeFurniture(piece, homeFurniture);
4207         if (homePieceOfFurniture != piece) {
4208           Area groupArea = null;
4209           if (lastGroup != homePieceOfFurniture) {
4210             Shape groupShape = ShapeTools.getShape(homePieceOfFurniture.getPoints(), true, null);
4211             groupArea = new Area(groupShape);
4212             // Enlarge group area
4213             groupArea.add(new Area(furnitureGroupsStroke.createStrokedShape(groupShape)));
4214           }
4215           Area pieceArea = new Area(ShapeTools.getShape(piece.getPoints(), true, null));
4216           if (furnitureGroupsArea == null) {
4217             furnitureGroupsArea = groupArea;
4218             furnitureInGroupsArea = pieceArea;
4219           } else {
4220             if (lastGroup != homePieceOfFurniture) {
4221               furnitureGroupsArea.add(groupArea);
4222             }
4223             furnitureInGroupsArea.add(pieceArea);
4224           }
4225           // Store last group to avoid useless multiple computation
4226           lastGroup = homePieceOfFurniture;
4227         }
4228       } else {
4229         it.remove();
4230       }
4231     }
4232     if (furnitureGroupsArea != null) {
4233       // Fill the area of furniture groups around items with light outine color
4234       furnitureGroupsArea.subtract(furnitureInGroupsArea);
4235       Composite oldComposite = setTransparency(g2D, 0.6f);
4236       g2D.setPaint(selectionOutlinePaint);
4237       g2D.fill(furnitureGroupsArea);
4238       g2D.setComposite(oldComposite);
4239     }
4240 
4241     for (HomePieceOfFurniture piece : furniture) {
4242       float [][] points = piece.getPoints();
4243       Shape pieceShape = ShapeTools.getShape(points, true, null);
4244 
4245       // Draw selection border
4246       g2D.setPaint(selectionOutlinePaint);
4247       g2D.setStroke(selectionOutlineStroke);
4248       g2D.draw(pieceShape);
4249 
4250       // Draw its border
4251       g2D.setPaint(foregroundColor);
4252       g2D.setStroke(pieceBorderStroke);
4253       g2D.draw(pieceShape);
4254 
4255       // Draw its front face with a thicker line
4256       g2D.setStroke(pieceFrontBorderStroke);
4257       g2D.draw(new Line2D.Float(points [2][0], points [2][1], points [3][0], points [3][1]));
4258 
4259       if (items.size() == 1 && indicatorPaint != null) {
4260         paintPieceOFFurnitureIndicators(g2D, piece, indicatorPaint, planScale);
4261       }
4262     }
4263   }
4264 
4265   /**
4266    * Returns <code>piece</code> if it belongs to home furniture or the group to which <code>piece</code> belongs.
4267    */
getPieceOfFurnitureInHomeFurniture(HomePieceOfFurniture piece, List<HomePieceOfFurniture> homeFurniture)4268   private HomePieceOfFurniture getPieceOfFurnitureInHomeFurniture(HomePieceOfFurniture piece,
4269                                                                   List<HomePieceOfFurniture> homeFurniture) {
4270     // Prefer iterate twice the furniture list rather than calling getAllFurniture uselessly
4271     // because subselecting won't happen often
4272     if (!homeFurniture.contains(piece)) {
4273       for (HomePieceOfFurniture homePiece : homeFurniture) {
4274         if (homePiece instanceof HomeFurnitureGroup
4275             && ((HomeFurnitureGroup)homePiece).getAllFurniture().contains(piece)) {
4276           return homePiece;
4277         }
4278       }
4279     }
4280     return piece;
4281   }
4282 
4283   /**
4284    * Paints <code>piece</code> icon with <code>g2D</code>.
4285    */
paintPieceOfFurnitureIcon(Graphics2D g2D, HomePieceOfFurniture piece, Shape pieceShape2D, float planScale, Color backgroundColor, PaintMode paintMode)4286   private void paintPieceOfFurnitureIcon(Graphics2D g2D, HomePieceOfFurniture piece,
4287                                          Shape pieceShape2D, float planScale,
4288                                          Color backgroundColor, PaintMode paintMode) {
4289     // Get piece icon
4290     Icon icon = IconManager.getInstance().getIcon(piece.getIcon(), 128,
4291         paintMode == PaintMode.PAINT ? this : null);
4292     paintPieceOfFurnitureIcon(g2D, piece, icon, pieceShape2D, planScale, backgroundColor);
4293   }
4294 
4295   /**
4296    * Paints <code>icon</code> with <code>g2D</code>.
4297    */
paintPieceOfFurnitureIcon(Graphics2D g2D, HomePieceOfFurniture piece, Icon icon, Shape pieceShape2D, float planScale, Color backgroundColor)4298   private void paintPieceOfFurnitureIcon(Graphics2D g2D, HomePieceOfFurniture piece, Icon icon,
4299                                          Shape pieceShape2D, float planScale, Color backgroundColor) {
4300     // Fill piece area
4301     g2D.setPaint(backgroundColor);
4302     g2D.fill(pieceShape2D);
4303 
4304     Shape previousClip = g2D.getClip();
4305     // Clip icon drawing into piece shape
4306     g2D.clip(pieceShape2D);
4307     AffineTransform previousTransform = g2D.getTransform();
4308     // Translate to piece center
4309     final Rectangle2D bounds = pieceShape2D.getBounds2D();
4310     g2D.translate(bounds.getCenterX(), bounds.getCenterY());
4311     float pieceDepth = piece.getDepthInPlan();
4312     if (piece instanceof HomeDoorOrWindow) {
4313       pieceDepth *= ((HomeDoorOrWindow)piece).getWallThickness();
4314     }
4315     // Scale icon to fit in its area
4316     float minDimension = Math.min(piece.getWidthInPlan(), pieceDepth);
4317     float iconScale = Math.min(1 / planScale, minDimension / icon.getIconHeight());
4318     // If piece model is mirrored, inverse x scale
4319     if (piece.isModelMirrored()) {
4320       g2D.scale(-iconScale, iconScale);
4321     } else {
4322       g2D.scale(iconScale, iconScale);
4323     }
4324     // Paint piece icon
4325     icon.paintIcon(this, g2D, -icon.getIconWidth() / 2, -icon.getIconHeight() / 2);
4326     // Revert g2D transformation to previous value
4327     g2D.setTransform(previousTransform);
4328     g2D.setClip(previousClip);
4329   }
4330 
4331   /**
4332    * Paints <code>piece</code> top icon with <code>g2D</code>.
4333    */
paintPieceOfFurnitureTop(Graphics2D g2D, HomePieceOfFurniture piece, Shape pieceShape2D, BasicStroke pieceBorderStroke, float planScale, Color backgroundColor, Color foregroundColor, PaintMode paintMode)4334   private void paintPieceOfFurnitureTop(Graphics2D g2D, HomePieceOfFurniture piece,
4335                                         Shape pieceShape2D, BasicStroke pieceBorderStroke,
4336                                         float planScale,
4337                                         Color backgroundColor, Color foregroundColor,
4338                                         PaintMode paintMode) {
4339     if (this.furnitureTopViewIconKeys == null) {
4340       this.furnitureTopViewIconKeys = new WeakHashMap<HomePieceOfFurniture, HomePieceOfFurnitureTopViewIconKey>();
4341       this.furnitureTopViewIconsCache = new WeakHashMap<HomePieceOfFurnitureTopViewIconKey, PieceOfFurnitureTopViewIcon>();
4342     }
4343     HomePieceOfFurnitureTopViewIconKey topViewIconKey = this.furnitureTopViewIconKeys.get(piece);
4344     PieceOfFurnitureTopViewIcon icon;
4345     if (topViewIconKey == null) {
4346       topViewIconKey = new HomePieceOfFurnitureTopViewIconKey(piece.clone());
4347       icon = this.furnitureTopViewIconsCache.get(topViewIconKey);
4348       if (icon == null
4349           || icon.isWaitIcon()
4350              && paintMode != PaintMode.PAINT) {
4351         PlanComponent waitingComponent = paintMode == PaintMode.PAINT ? this : null;
4352         // Prefer use plan icon if it exists
4353         if (piece.getPlanIcon() != null) {
4354           icon = new PieceOfFurniturePlanIcon(piece, waitingComponent);
4355         } else {
4356           icon = new PieceOfFurnitureModelIcon(piece, this.object3dFactory, waitingComponent, this.preferences.getFurnitureModelIconSize());
4357         }
4358         this.furnitureTopViewIconsCache.put(topViewIconKey, icon);
4359       } else {
4360         // As furnitureTopViewIconKeys and furnitureTopViewIconsCache are both WeakHashMap instances,
4361         // use the HomePieceOfFurnitureTopViewIconKey instance that already exists in furnitureTopViewIconsCache
4362         // to avoid the deletion of the entry containing the new sibling when a piece is garbage collected
4363         for (HomePieceOfFurnitureTopViewIconKey key : furnitureTopViewIconsCache.keySet()) {
4364           if (key.equals(topViewIconKey)) {
4365             topViewIconKey = key;
4366             break;
4367           }
4368         }
4369       }
4370       this.furnitureTopViewIconKeys.put(piece, topViewIconKey);
4371     } else {
4372       icon = this.furnitureTopViewIconsCache.get(topViewIconKey);
4373     }
4374 
4375     if (icon.isWaitIcon() || icon.isErrorIcon()) {
4376       paintPieceOfFurnitureIcon(g2D, piece, icon, pieceShape2D, planScale, backgroundColor);
4377       g2D.setPaint(foregroundColor);
4378       g2D.setStroke(pieceBorderStroke);
4379       g2D.draw(pieceShape2D);
4380     } else {
4381       AffineTransform previousTransform = g2D.getTransform();
4382       // Translate to piece center
4383       final Rectangle2D bounds = pieceShape2D.getBounds2D();
4384       g2D.translate(bounds.getCenterX(), bounds.getCenterY());
4385       g2D.rotate(piece.getAngle());
4386       float pieceDepth = piece.getDepthInPlan();
4387       // Scale icon to fit in its area
4388       if (piece.isModelMirrored()
4389           && piece.getRoll() == 0) {
4390         // If piece model is mirrored when its roll rotation is 0, inverse x scale
4391         g2D.scale(-piece.getWidthInPlan() / icon.getIconWidth(), pieceDepth / icon.getIconHeight());
4392       } else {
4393         g2D.scale(piece.getWidthInPlan() / icon.getIconWidth(), pieceDepth / icon.getIconHeight());
4394       }
4395       // Paint piece icon
4396       icon.paintIcon(this, g2D, -icon.getIconWidth() / 2, -icon.getIconHeight() / 2);
4397       // Revert g2D transformation to previous value
4398       g2D.setTransform(previousTransform);
4399     }
4400   }
4401 
4402   /**
4403    * Paints rotation, elevation, height and resize indicators on <code>piece</code>.
4404    */
paintPieceOFFurnitureIndicators(Graphics2D g2D, HomePieceOfFurniture piece, Paint indicatorPaint, float planScale)4405   private void paintPieceOFFurnitureIndicators(Graphics2D g2D,
4406                                                HomePieceOfFurniture piece,
4407                                                Paint indicatorPaint,
4408                                                float planScale) {
4409     if (this.resizeIndicatorVisible) {
4410       g2D.setPaint(indicatorPaint);
4411       g2D.setStroke(INDICATOR_STROKE);
4412 
4413       AffineTransform previousTransform = g2D.getTransform();
4414       float [][] piecePoints = piece.getPoints();
4415       float scaleInverse = 1 / planScale;
4416       float pieceAngle = piece.getAngle();
4417       Shape rotationIndicator = getIndicator(piece, IndicatorType.ROTATE);
4418       if (rotationIndicator != null) {
4419         // Draw rotation indicator at top left point of the piece
4420         g2D.translate(piecePoints [0][0], piecePoints [0][1]);
4421         g2D.scale(scaleInverse, scaleInverse);
4422         g2D.rotate(pieceAngle);
4423         g2D.draw(rotationIndicator);
4424         g2D.setTransform(previousTransform);
4425       }
4426 
4427       Shape elevationIndicator = getIndicator(piece, IndicatorType.ELEVATE);
4428       if (elevationIndicator != null) {
4429         // Draw elevation indicator at top right point of the piece
4430         g2D.translate(piecePoints [1][0], piecePoints [1][1]);
4431         g2D.scale(scaleInverse, scaleInverse);
4432         g2D.rotate(pieceAngle);
4433         g2D.draw(ELEVATION_POINT_INDICATOR);
4434         // Place elevation indicator farther but don't rotate it
4435         g2D.translate(6.5f, -6.5f);
4436         g2D.rotate(-pieceAngle);
4437         g2D.draw(elevationIndicator);
4438         g2D.setTransform(previousTransform);
4439       }
4440 
4441       // Draw pitch, roll, light or height indicator at bottom left point of the piece
4442       g2D.translate(piecePoints [3][0], piecePoints [3][1]);
4443       g2D.scale(scaleInverse, scaleInverse);
4444       g2D.rotate(pieceAngle);
4445       if (piece.getPitch() != 0
4446           && isFurnitureSizeInPlanSupported()) {
4447         Shape pitchIndicator = getIndicator(piece, IndicatorType.ROTATE_PITCH);
4448         if (pitchIndicator != null) {
4449           g2D.draw(pitchIndicator);
4450         }
4451       } else if (piece.getRoll() != 0
4452                 && isFurnitureSizeInPlanSupported()) {
4453         Shape rollIndicator = getIndicator(piece, IndicatorType.ROTATE_ROLL);
4454         if (rollIndicator != null) {
4455           g2D.draw(rollIndicator);
4456         }
4457       } else if (piece instanceof HomeLight) {
4458         Shape powerIndicator = getIndicator(piece, IndicatorType.CHANGE_POWER);
4459         if (powerIndicator != null) {
4460           g2D.draw(LIGHT_POWER_POINT_INDICATOR);
4461           // Place power indicator farther but don't rotate it
4462           g2D.translate(-7.5f, 7.5f);
4463           g2D.rotate(-pieceAngle);
4464           g2D.draw(powerIndicator);
4465         }
4466       } else if (piece.isResizable()
4467                  && !piece.isHorizontallyRotated()) {
4468         Shape heightIndicator = getIndicator(piece, IndicatorType.RESIZE_HEIGHT);
4469         if (heightIndicator != null) {
4470           g2D.draw(FURNITURE_HEIGHT_POINT_INDICATOR);
4471           // Place height indicator farther but don't rotate it
4472           g2D.translate(-7.5f, 7.5f);
4473           g2D.rotate(-pieceAngle);
4474           g2D.draw(heightIndicator);
4475         }
4476       }
4477       g2D.setTransform(previousTransform);
4478 
4479       if (piece.isResizable()) {
4480         Shape resizeIndicator = getIndicator(piece, IndicatorType.RESIZE);
4481         if (resizeIndicator != null) {
4482           // Draw resize indicator at top left point of the piece
4483           g2D.translate(piecePoints [2][0], piecePoints [2][1]);
4484           g2D.scale(scaleInverse, scaleInverse);
4485           g2D.rotate(pieceAngle);
4486           g2D.draw(resizeIndicator);
4487           g2D.setTransform(previousTransform);
4488         }
4489       }
4490 
4491       if (piece.isNameVisible()
4492           && piece.getName().trim().length() > 0) {
4493         float xName = piece.getX() + piece.getNameXOffset();
4494         float yName = piece.getY() + piece.getNameYOffset();
4495         paintTextIndicators(g2D, piece.getClass(), getLineCount(piece.getName()),
4496             piece.getNameStyle(), xName, yName, piece.getNameAngle(), indicatorPaint, planScale);
4497       }
4498     }
4499   }
4500 
4501   /**
4502    * Paints polylines.
4503    */
paintPolylines(Graphics2D g2D, Collection<Polyline> polylines, List<Selectable> selectedItems, Paint selectionOutlinePaint, Paint indicatorPaint, float planScale, Color foregroundColor, PaintMode paintMode)4504   private void paintPolylines(Graphics2D g2D,
4505                               Collection<Polyline> polylines, List<Selectable> selectedItems,
4506                               Paint selectionOutlinePaint,
4507                               Paint indicatorPaint, float planScale,
4508                               Color foregroundColor, PaintMode paintMode) {
4509     // Draw polylines
4510     for (Polyline polyline : polylines) {
4511       if (isViewableAtSelectedLevel(polyline)) {
4512         boolean selected = selectedItems.contains(polyline);
4513         if (paintMode != PaintMode.CLIPBOARD
4514             || selected) {
4515           g2D.setPaint(new Color(polyline.getColor()));
4516           float thickness = polyline.getThickness();
4517           g2D.setStroke(ShapeTools.getStroke(thickness, polyline.getCapStyle(), polyline.getJoinStyle(),
4518               polyline.getDashStyle() != Polyline.DashStyle.SOLID ? polyline.getDashPattern() : null, // null renders better closed shapes with a solid style
4519               polyline.getDashOffset()));
4520           Shape polylineShape = ShapeTools.getPolylineShape(polyline.getPoints(),
4521               polyline.getJoinStyle() == Polyline.JoinStyle.CURVED, polyline.isClosedPath());
4522           g2D.draw(polylineShape);
4523 
4524           // Search angle at start and at end
4525           float [] firstPoint = null;
4526           float [] secondPoint = null;
4527           float [] beforeLastPoint = null;
4528           float [] lastPoint = null;
4529           for (PathIterator it = polylineShape.getPathIterator(null, 0.5); !it.isDone(); it.next()) {
4530             float [] pathPoint = new float [2];
4531             if (it.currentSegment(pathPoint) != PathIterator.SEG_CLOSE) {
4532               if (firstPoint == null) {
4533                 firstPoint = pathPoint;
4534               } else if (secondPoint == null) {
4535                 secondPoint = pathPoint;
4536               }
4537               beforeLastPoint = lastPoint;
4538               lastPoint = pathPoint;
4539             }
4540           }
4541           float angleAtStart = (float)Math.atan2(firstPoint [1] - secondPoint [1],
4542               firstPoint [0] - secondPoint [0]);
4543           float angleAtEnd = (float)Math.atan2(lastPoint [1] - beforeLastPoint [1],
4544               lastPoint [0] - beforeLastPoint [0]);
4545           float arrowDelta = polyline.getCapStyle() != Polyline.CapStyle.BUTT
4546               ? thickness / 2
4547               : 0;
4548           paintArrow(g2D, firstPoint, angleAtStart, polyline.getStartArrowStyle(), thickness, arrowDelta);
4549           paintArrow(g2D, lastPoint, angleAtEnd, polyline.getEndArrowStyle(), thickness, arrowDelta);
4550 
4551           if (selected
4552               && paintMode == PaintMode.PAINT) {
4553             g2D.setPaint(selectionOutlinePaint);
4554             g2D.setStroke(SwingTools.getStroke(thickness + 4 / planScale,
4555                 polyline.getCapStyle(), polyline.getJoinStyle(), Polyline.DashStyle.SOLID));
4556             g2D.draw(polylineShape);
4557 
4558             // Paint resize indicators of the polyline if indicator paint exists
4559             if (selectedItems.size() == 1
4560                 && indicatorPaint != null) {
4561               Polyline selectedPolyline = (Polyline)selectedItems.get(0);
4562               if (isViewableAtSelectedLevel(selectedPolyline)) {
4563                 g2D.setPaint(indicatorPaint);
4564                 paintPointsResizeIndicators(g2D, selectedPolyline, indicatorPaint, planScale,
4565                     selectedPolyline.isClosedPath(), angleAtStart, angleAtEnd, false);
4566               }
4567             }
4568           }
4569         }
4570       }
4571     }
4572   }
4573 
4574   /**
4575    * Paints polyline arrow at the given point and orientation.
4576    */
paintArrow(Graphics2D g2D, float [] point, float angle, Polyline.ArrowStyle arrowStyle, float thickness, float arrowDelta)4577   private void paintArrow(Graphics2D g2D, float [] point, float angle,
4578                           Polyline.ArrowStyle arrowStyle, float thickness, float arrowDelta) {
4579     if (arrowStyle != null
4580         && arrowStyle != Polyline.ArrowStyle.NONE) {
4581       AffineTransform oldTransform = g2D.getTransform();
4582       g2D.translate(point [0], point [1]);
4583       g2D.rotate(angle);
4584       g2D.translate(arrowDelta, 0);
4585       double scale = Math.pow(thickness, 0.66f) * 2;
4586       g2D.scale(scale, scale);
4587       switch (arrowStyle) {
4588         case DISC :
4589           g2D.fill(new Ellipse2D.Float(-3.5f, -2, 4, 4));
4590           break;
4591         case OPEN :
4592           g2D.scale(0.9, 0.9);
4593           g2D.setStroke(new BasicStroke((float)(thickness / scale / 0.9), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
4594           g2D.draw(ARROW);
4595           break;
4596         case DELTA :
4597           g2D.translate(1.65f, 0);
4598           g2D.fill(ARROW);
4599           break;
4600         default:
4601           break;
4602       }
4603       g2D.setTransform(oldTransform);
4604     }
4605   }
4606 
4607   /**
4608    * Paints dimension lines.
4609    */
paintDimensionLines(Graphics2D g2D, Collection<DimensionLine> dimensionLines, List<Selectable> selectedItems, Paint selectionOutlinePaint, Stroke selectionOutlineStroke, Paint indicatorPaint, Stroke extensionLineStroke, float planScale, Color backgroundColor, Color foregroundColor, PaintMode paintMode, boolean feedback)4610   private void paintDimensionLines(Graphics2D g2D,
4611                           Collection<DimensionLine> dimensionLines, List<Selectable> selectedItems,
4612                           Paint selectionOutlinePaint, Stroke selectionOutlineStroke,
4613                           Paint indicatorPaint, Stroke extensionLineStroke, float planScale,
4614                           Color backgroundColor, Color foregroundColor,
4615                           PaintMode paintMode, boolean feedback) {
4616     // In clipboard paint mode, paint only selected dimension lines
4617     if (paintMode == PaintMode.CLIPBOARD) {
4618       dimensionLines = Home.getDimensionLinesSubList(selectedItems);
4619     }
4620 
4621     // Draw dimension lines
4622     g2D.setPaint(foregroundColor);
4623     BasicStroke dimensionLineStroke = new BasicStroke(getStrokeWidth(DimensionLine.class, paintMode) / planScale);
4624     // Change font size
4625     Font previousFont = g2D.getFont();
4626     for (DimensionLine dimensionLine : dimensionLines) {
4627       if (isViewableAtSelectedLevel(dimensionLine)) {
4628         AffineTransform previousTransform = g2D.getTransform();
4629         double angle = Math.atan2(dimensionLine.getYEnd() - dimensionLine.getYStart(),
4630             dimensionLine.getXEnd() - dimensionLine.getXStart());
4631         float dimensionLineLength = dimensionLine.getLength();
4632         g2D.translate(dimensionLine.getXStart(), dimensionLine.getYStart());
4633         g2D.rotate(angle);
4634         g2D.translate(0, dimensionLine.getOffset());
4635 
4636         if (paintMode == PaintMode.PAINT
4637             && this.selectedItemsOutlinePainted
4638             && selectedItems.contains(dimensionLine)) {
4639           // Draw selection border
4640           g2D.setPaint(selectionOutlinePaint);
4641           g2D.setStroke(selectionOutlineStroke);
4642           // Draw dimension line
4643           g2D.draw(new Line2D.Float(0, 0, dimensionLineLength, 0));
4644           // Draw dimension line ends
4645           g2D.draw(DIMENSION_LINE_END);
4646           g2D.translate(dimensionLineLength, 0);
4647           g2D.draw(DIMENSION_LINE_END);
4648           g2D.translate(-dimensionLineLength, 0);
4649           // Draw extension lines
4650           g2D.draw(new Line2D.Float(0, -dimensionLine.getOffset(), 0, -5));
4651           g2D.draw(new Line2D.Float(dimensionLineLength, -dimensionLine.getOffset(), dimensionLineLength, -5));
4652 
4653           g2D.setPaint(foregroundColor);
4654         }
4655 
4656         g2D.setStroke(dimensionLineStroke);
4657         // Draw dimension line
4658         g2D.draw(new Line2D.Float(0, 0, dimensionLineLength, 0));
4659         // Draw dimension line ends
4660         g2D.draw(DIMENSION_LINE_END);
4661         g2D.translate(dimensionLineLength, 0);
4662         g2D.draw(DIMENSION_LINE_END);
4663         g2D.translate(-dimensionLineLength, 0);
4664         // Draw extension lines
4665         g2D.setStroke(extensionLineStroke);
4666         g2D.draw(new Line2D.Float(0, -dimensionLine.getOffset(), 0, -5));
4667         g2D.draw(new Line2D.Float(dimensionLineLength, -dimensionLine.getOffset(), dimensionLineLength, -5));
4668 
4669         String lengthText = this.preferences.getLengthUnit().getFormat().format(dimensionLineLength);
4670         TextStyle lengthStyle = dimensionLine.getLengthStyle();
4671         if (lengthStyle == null) {
4672           lengthStyle = this.preferences.getDefaultTextStyle(dimensionLine.getClass());
4673         }
4674         if (feedback && getFont() != null) {
4675           // Use default for feedback
4676           lengthStyle = lengthStyle.deriveStyle(getFont().getSize() / planScale / resolutionScale);
4677         }
4678         Font font = getFont(previousFont, lengthStyle);
4679         FontMetrics lengthFontMetrics = getFontMetrics(font, lengthStyle);
4680         Rectangle2D lengthTextBounds = lengthFontMetrics.getStringBounds(lengthText, g2D);
4681         int fontAscent = lengthFontMetrics.getAscent();
4682         g2D.translate((dimensionLineLength - (float)lengthTextBounds.getWidth()) / 2,
4683             dimensionLine.getOffset() <= 0
4684                 ? -lengthFontMetrics.getDescent() - 1
4685                 : fontAscent + 1);
4686         if (feedback) {
4687           // Draw text outline with half transparent background color
4688           g2D.setPaint(backgroundColor);
4689           Composite oldComposite = setTransparency(g2D, 0.7f);
4690           g2D.setStroke(new BasicStroke(4 / planScale, BasicStroke.CAP_SQUARE, BasicStroke.CAP_ROUND));
4691           FontRenderContext fontRenderContext = g2D.getFontRenderContext();
4692           TextLayout textLayout = new TextLayout(lengthText, font, fontRenderContext);
4693           g2D.draw(textLayout.getOutline(new AffineTransform()));
4694           g2D.setComposite(oldComposite);
4695           g2D.setPaint(foregroundColor);
4696         }
4697         // Draw dimension length in middle
4698         g2D.setFont(font);
4699         g2D.drawString(lengthText, 0, 0);
4700 
4701         g2D.setTransform(previousTransform);
4702       }
4703     }
4704     g2D.setFont(previousFont);
4705     // Paint resize indicator of selected dimension line
4706     if (selectedItems.size() == 1
4707         && selectedItems.get(0) instanceof DimensionLine
4708         && paintMode == PaintMode.PAINT
4709         && indicatorPaint != null) {
4710       paintDimensionLineResizeIndicator(g2D, (DimensionLine)selectedItems.get(0), indicatorPaint, planScale);
4711     }
4712   }
4713 
4714   /**
4715    * Paints resize indicator on a given dimension line.
4716    */
paintDimensionLineResizeIndicator(Graphics2D g2D, DimensionLine dimensionLine, Paint indicatorPaint, float planScale)4717   private void paintDimensionLineResizeIndicator(Graphics2D g2D, DimensionLine dimensionLine,
4718                                                  Paint indicatorPaint,
4719                                                  float planScale) {
4720     if (this.resizeIndicatorVisible) {
4721       g2D.setPaint(indicatorPaint);
4722       g2D.setStroke(INDICATOR_STROKE);
4723 
4724       double wallAngle = Math.atan2(dimensionLine.getYEnd() - dimensionLine.getYStart(),
4725           dimensionLine.getXEnd() - dimensionLine.getXStart());
4726 
4727       AffineTransform previousTransform = g2D.getTransform();
4728       float scaleInverse = 1 / planScale;
4729       // Draw resize indicator at the start of dimension line
4730       g2D.translate(dimensionLine.getXStart(), dimensionLine.getYStart());
4731       g2D.rotate(wallAngle);
4732       g2D.translate(0, dimensionLine.getOffset());
4733       g2D.rotate(Math.PI);
4734       g2D.scale(scaleInverse, scaleInverse);
4735       Shape resizeIndicator = getIndicator(dimensionLine, IndicatorType.RESIZE);
4736       g2D.draw(resizeIndicator);
4737       g2D.setTransform(previousTransform);
4738 
4739       // Draw resize indicator at the end of dimension line
4740       g2D.translate(dimensionLine.getXEnd(), dimensionLine.getYEnd());
4741       g2D.rotate(wallAngle);
4742       g2D.translate(0, dimensionLine.getOffset());
4743       g2D.scale(scaleInverse, scaleInverse);
4744       g2D.draw(resizeIndicator);
4745       g2D.setTransform(previousTransform);
4746 
4747       // Draw resize indicator at the middle of dimension line
4748       g2D.translate((dimensionLine.getXStart() + dimensionLine.getXEnd()) / 2,
4749           (dimensionLine.getYStart() + dimensionLine.getYEnd()) / 2);
4750       g2D.rotate(wallAngle);
4751       g2D.translate(0, dimensionLine.getOffset());
4752       g2D.rotate(dimensionLine.getOffset() <= 0
4753           ? Math.PI / 2
4754           : -Math.PI / 2);
4755       g2D.scale(scaleInverse, scaleInverse);
4756       g2D.draw(resizeIndicator);
4757       g2D.setTransform(previousTransform);
4758     }
4759   }
4760 
4761   /**
4762    * Paints home labels.
4763    */
paintLabels(Graphics2D g2D, Collection<Label> labels, List<Selectable> selectedItems, Paint selectionOutlinePaint, Stroke selectionOutlineStroke, Paint indicatorPaint, float planScale, Color foregroundColor, PaintMode paintMode)4764   private void paintLabels(Graphics2D g2D, Collection<Label> labels, List<Selectable> selectedItems,
4765                            Paint selectionOutlinePaint, Stroke selectionOutlineStroke, Paint indicatorPaint,
4766                            float planScale, Color foregroundColor, PaintMode paintMode) {
4767     Font previousFont = g2D.getFont();
4768     // Draw labels
4769     for (Label label : labels) {
4770       if (isViewableAtSelectedLevel(label)) {
4771         boolean selectedLabel = selectedItems.contains(label);
4772         // In clipboard paint mode, paint label only if it is selected
4773         if (paintMode != PaintMode.CLIPBOARD || selectedLabel) {
4774           String labelText = label.getText();
4775           float xLabel = label.getX();
4776           float yLabel = label.getY();
4777           float labelAngle = label.getAngle();
4778           TextStyle labelStyle = label.getStyle();
4779           if (labelStyle == null) {
4780             labelStyle = this.preferences.getDefaultTextStyle(label.getClass());
4781           }
4782           if (labelStyle.getFontName() == null && getFont() != null) {
4783             labelStyle = labelStyle.deriveStyle(getFont().getFontName());
4784           }
4785           Integer color = label.getColor();
4786           g2D.setPaint(color != null ?  new Color(color) : foregroundColor);
4787           paintText(g2D, label.getClass(), labelText, labelStyle, label.getOutlineColor(),
4788               xLabel, yLabel, labelAngle, previousFont);
4789 
4790           if (paintMode == PaintMode.PAINT && this.selectedItemsOutlinePainted && selectedLabel) {
4791             // Draw selection border
4792             g2D.setPaint(selectionOutlinePaint);
4793             g2D.setStroke(selectionOutlineStroke);
4794             float [][] textBounds = getTextBounds(labelText, labelStyle, xLabel, yLabel, labelAngle);
4795             g2D.draw(ShapeTools.getShape(textBounds, true, null));
4796             g2D.setPaint(foregroundColor);
4797             if (indicatorPaint != null
4798                 && selectedItems.size() == 1
4799                 && selectedItems.get(0) == label) {
4800               paintTextIndicators(g2D, label.getClass(), getLineCount(labelText),
4801                   labelStyle, xLabel, yLabel, labelAngle, indicatorPaint, planScale);
4802 
4803               if (this.resizeIndicatorVisible
4804                   && label.getPitch() != null) {
4805                 Shape elevationIndicator = getIndicator(label, IndicatorType.ELEVATE);
4806                 if (elevationIndicator != null) {
4807                   AffineTransform previousTransform = g2D.getTransform();
4808                   // Draw elevation indicator bellow rotation center
4809                   if (labelStyle.getAlignment() == TextStyle.Alignment.LEFT) {
4810                     g2D.translate(textBounds [3][0], textBounds [3][1]);
4811                   } else if (labelStyle.getAlignment() == TextStyle.Alignment.RIGHT) {
4812                     g2D.translate(textBounds [2][0], textBounds [2][1]);
4813                   } else { // CENTER
4814                     g2D.translate((textBounds [2][0] + textBounds [3][0]) / 2, (textBounds [2][1] + textBounds [3][1]) / 2);
4815                   }
4816                   float scaleInverse = 1 / planScale;
4817                   g2D.scale(scaleInverse, scaleInverse);
4818                   g2D.rotate(label.getAngle());
4819                   g2D.draw(ELEVATION_POINT_INDICATOR);
4820                   // Place elevation indicator farther but don't rotate it
4821                   g2D.translate(0, 10f);
4822                   g2D.rotate(-label.getAngle());
4823                   g2D.draw(elevationIndicator);
4824                   g2D.setTransform(previousTransform);
4825                 }
4826               }
4827             }
4828           }
4829         }
4830       }
4831     }
4832     g2D.setFont(previousFont);
4833   }
4834 
4835   /**
4836    * Paints the compass.
4837    */
paintCompass(Graphics2D g2D, List<Selectable> selectedItems, float planScale, Color foregroundColor, PaintMode paintMode)4838   private void paintCompass(Graphics2D g2D, List<Selectable> selectedItems, float planScale,
4839                             Color foregroundColor, PaintMode paintMode) {
4840     Compass compass = this.home.getCompass();
4841     if (compass.isVisible()
4842         && (paintMode != PaintMode.CLIPBOARD
4843             || selectedItems.contains(compass))) {
4844       AffineTransform previousTransform = g2D.getTransform();
4845       g2D.translate(compass.getX(), compass.getY());
4846       g2D.rotate(compass.getNorthDirection());
4847       float diameter = compass.getDiameter();
4848       g2D.scale(diameter, diameter);
4849       g2D.setColor(foregroundColor);
4850       g2D.fill(COMPASS);
4851       g2D.setTransform(previousTransform);
4852     }
4853   }
4854 
4855   /**
4856    * Paints the outline of the compass when it's belongs to <code>items</code>.
4857    */
paintCompassOutline(Graphics2D g2D, List<Selectable> items, Paint selectionOutlinePaint, Stroke selectionOutlineStroke, Paint indicatorPaint, float planScale, Color foregroundColor)4858   private void paintCompassOutline(Graphics2D g2D, List<Selectable> items,
4859                                    Paint selectionOutlinePaint, Stroke selectionOutlineStroke,
4860                                    Paint indicatorPaint, float planScale, Color foregroundColor) {
4861     Compass compass = this.home.getCompass();
4862     if (items.contains(compass)
4863         && compass.isVisible()) {
4864       AffineTransform previousTransform = g2D.getTransform();
4865       g2D.translate(compass.getX(), compass.getY());
4866       g2D.rotate(compass.getNorthDirection());
4867       float diameter = compass.getDiameter();
4868       g2D.scale(diameter, diameter);
4869 
4870       g2D.setPaint(selectionOutlinePaint);
4871       g2D.setStroke(new BasicStroke((5.5f + planScale) / diameter / planScale));
4872       g2D.draw(COMPASS_DISC);
4873       g2D.setColor(foregroundColor);
4874       g2D.setStroke(new BasicStroke(1f / diameter / planScale));
4875       g2D.draw(COMPASS_DISC);
4876       g2D.setTransform(previousTransform);
4877 
4878       // Paint indicators of the compass
4879       if (items.size() == 1
4880           && items.get(0) == compass) {
4881         g2D.setPaint(indicatorPaint);
4882         paintCompassIndicators(g2D, compass, indicatorPaint, planScale);
4883       }
4884     }
4885   }
4886 
paintCompassIndicators(Graphics2D g2D, Compass compass, Paint indicatorPaint, float planScale)4887   private void paintCompassIndicators(Graphics2D g2D,
4888                                       Compass compass, Paint indicatorPaint,
4889                                       float planScale) {
4890     if (this.resizeIndicatorVisible) {
4891       g2D.setPaint(indicatorPaint);
4892       g2D.setStroke(INDICATOR_STROKE);
4893 
4894       AffineTransform previousTransform = g2D.getTransform();
4895       // Draw rotation indicator at middle of second and third point of compass
4896       float [][] compassPoints = compass.getPoints();
4897       float scaleInverse = 1 / planScale;
4898       g2D.translate((compassPoints [2][0] + compassPoints [3][0]) / 2,
4899           (compassPoints [2][1] + compassPoints [3][1]) / 2);
4900       g2D.scale(scaleInverse, scaleInverse);
4901       g2D.rotate(compass.getNorthDirection());
4902       g2D.draw(getIndicator(compass, IndicatorType.ROTATE));
4903       g2D.setTransform(previousTransform);
4904 
4905       // Draw resize indicator at middle of second and third point of compass
4906       g2D.translate((compassPoints [1][0] + compassPoints [2][0]) / 2,
4907           (compassPoints [1][1] + compassPoints [2][1]) / 2);
4908       g2D.scale(scaleInverse, scaleInverse);
4909       g2D.rotate(compass.getNorthDirection());
4910       g2D.draw(getIndicator(compass, IndicatorType.RESIZE));
4911       g2D.setTransform(previousTransform);
4912     }
4913   }
4914 
4915   /**
4916    * Paints wall location feedback.
4917    */
paintWallAlignmentFeedback(Graphics2D g2D, Wall alignedWall, Point2D locationFeedback, boolean showPointFeedback, Paint feedbackPaint, Stroke feedbackStroke, float planScale, Paint pointPaint, Stroke pointStroke)4918   private void paintWallAlignmentFeedback(Graphics2D g2D,
4919                                           Wall alignedWall, Point2D locationFeedback,
4920                                           boolean showPointFeedback,
4921                                           Paint feedbackPaint, Stroke feedbackStroke,
4922                                           float planScale, Paint pointPaint,
4923                                           Stroke pointStroke) {
4924     // Paint wall location feedback
4925     if (locationFeedback != null) {
4926       float margin = 0.5f / planScale;
4927       // Search which wall start or end point is at locationFeedback abscissa or ordinate
4928       // ignoring the start and end point of alignedWall
4929       float x = (float)locationFeedback.getX();
4930       float y = (float)locationFeedback.getY();
4931       float deltaXToClosestWall = Float.POSITIVE_INFINITY;
4932       float deltaYToClosestWall = Float.POSITIVE_INFINITY;
4933       for (Wall wall : getViewedItems(this.home.getWalls(), this.otherLevelsWallsCache)) {
4934         if (wall != alignedWall) {
4935           if (Math.abs(x - wall.getXStart()) < margin
4936               && (alignedWall == null
4937                   || !equalsWallPoint(wall.getXStart(), wall.getYStart(), alignedWall))) {
4938             if (Math.abs(deltaYToClosestWall) > Math.abs(y - wall.getYStart())) {
4939               deltaYToClosestWall = y - wall.getYStart();
4940             }
4941           } else if (Math.abs(x - wall.getXEnd()) < margin
4942                     && (alignedWall == null
4943                         || !equalsWallPoint(wall.getXEnd(), wall.getYEnd(), alignedWall))) {
4944             if (Math.abs(deltaYToClosestWall) > Math.abs(y - wall.getYEnd())) {
4945               deltaYToClosestWall = y - wall.getYEnd();
4946             }
4947           }
4948 
4949           if (Math.abs(y - wall.getYStart()) < margin
4950               && (alignedWall == null
4951                   || !equalsWallPoint(wall.getXStart(), wall.getYStart(), alignedWall))) {
4952             if (Math.abs(deltaXToClosestWall) > Math.abs(x - wall.getXStart())) {
4953               deltaXToClosestWall = x - wall.getXStart();
4954             }
4955           } else if (Math.abs(y - wall.getYEnd()) < margin
4956                     && (alignedWall == null
4957                         || !equalsWallPoint(wall.getXEnd(), wall.getYEnd(), alignedWall))) {
4958             if (Math.abs(deltaXToClosestWall) > Math.abs(x - wall.getXEnd())) {
4959               deltaXToClosestWall = x - wall.getXEnd();
4960             }
4961           }
4962 
4963           float [][] wallPoints = wall.getPoints();
4964           // Take into account only points at start and end of the wall
4965           wallPoints = new float [][] {wallPoints [0], wallPoints [wallPoints.length / 2 - 1],
4966                                        wallPoints [wallPoints.length / 2], wallPoints [wallPoints.length - 1]};
4967           for (int i = 0; i < wallPoints.length; i++) {
4968             if (Math.abs(x - wallPoints [i][0]) < margin
4969                 && (alignedWall == null
4970                     || !equalsWallPoint(wallPoints [i][0], wallPoints [i][1], alignedWall))) {
4971               if (Math.abs(deltaYToClosestWall) > Math.abs(y - wallPoints [i][1])) {
4972                 deltaYToClosestWall = y - wallPoints [i][1];
4973               }
4974             }
4975             if (Math.abs(y - wallPoints [i][1]) < margin
4976                 && (alignedWall == null
4977                     || !equalsWallPoint(wallPoints [i][0], wallPoints [i][1], alignedWall))) {
4978               if (Math.abs(deltaXToClosestWall) > Math.abs(x - wallPoints [i][0])) {
4979                 deltaXToClosestWall = x - wallPoints [i][0];
4980               }
4981             }
4982           }
4983         }
4984       }
4985 
4986       // Draw alignment horizontal and vertical lines
4987       g2D.setPaint(feedbackPaint);
4988       g2D.setStroke(feedbackStroke);
4989       if (deltaXToClosestWall != Float.POSITIVE_INFINITY) {
4990         if (deltaXToClosestWall > 0) {
4991           g2D.draw(new Line2D.Float(x + ALIGNMENT_LINE_OFFSET / planScale, y,
4992               x - deltaXToClosestWall - ALIGNMENT_LINE_OFFSET / planScale, y));
4993         } else {
4994           g2D.draw(new Line2D.Float(x - ALIGNMENT_LINE_OFFSET / planScale, y,
4995               x - deltaXToClosestWall + ALIGNMENT_LINE_OFFSET / planScale, y));
4996         }
4997       }
4998 
4999       if (deltaYToClosestWall != Float.POSITIVE_INFINITY) {
5000         if (deltaYToClosestWall > 0) {
5001           g2D.draw(new Line2D.Float(x, y + ALIGNMENT_LINE_OFFSET / planScale,
5002               x, y - deltaYToClosestWall - ALIGNMENT_LINE_OFFSET / planScale));
5003         } else {
5004           g2D.draw(new Line2D.Float(x, y - ALIGNMENT_LINE_OFFSET / planScale,
5005               x, y - deltaYToClosestWall + ALIGNMENT_LINE_OFFSET / planScale));
5006         }
5007       }
5008 
5009       // Draw point feedback
5010       if (showPointFeedback) {
5011         paintPointFeedback(g2D, locationFeedback, feedbackPaint, planScale, pointPaint, pointStroke);
5012       }
5013     }
5014   }
5015 
5016   /**
5017    * Returns the items viewed in the plan at the selected level.
5018    */
getViewedItems(Collection<T> homeItems, List<T> otherLevelItems)5019   private <T extends Elevatable> Collection<T> getViewedItems(Collection<T> homeItems, List<T> otherLevelItems) {
5020     List<T> viewedWalls = new ArrayList<T>();
5021     if (otherLevelItems != null) {
5022       viewedWalls.addAll(otherLevelItems);
5023     }
5024     for (T wall : homeItems) {
5025       if (isViewableAtSelectedLevel(wall)) {
5026         viewedWalls.add(wall);
5027       }
5028     }
5029     return viewedWalls;
5030   }
5031 
5032   /**
5033    * Paints point feedback.
5034    */
paintPointFeedback(Graphics2D g2D, Point2D locationFeedback, Paint feedbackPaint, float planScale, Paint pointPaint, Stroke pointStroke)5035   private void paintPointFeedback(Graphics2D g2D, Point2D locationFeedback,
5036                                   Paint feedbackPaint, float planScale,
5037                                   Paint pointPaint, Stroke pointStroke) {
5038     g2D.setPaint(pointPaint);
5039     g2D.setStroke(pointStroke);
5040     float radius = 10;
5041     Ellipse2D.Float circle = new Ellipse2D.Float((float)locationFeedback.getX() - radius / planScale,
5042         (float)locationFeedback.getY() - radius / planScale, 2 * radius / planScale, 2 * radius / planScale);
5043     g2D.fill(circle);
5044     g2D.setPaint(feedbackPaint);
5045     g2D.setStroke(new BasicStroke(1 / planScale));
5046     g2D.draw(circle);
5047     g2D.draw(new Line2D.Float((float)locationFeedback.getX(),
5048         (float)locationFeedback.getY() - radius / planScale,
5049         (float)locationFeedback.getX(),
5050         (float)locationFeedback.getY() + radius / planScale));
5051     g2D.draw(new Line2D.Float((float)locationFeedback.getX() - radius / planScale,
5052         (float)locationFeedback.getY(),
5053         (float)locationFeedback.getX() + radius / planScale,
5054         (float)locationFeedback.getY()));
5055   }
5056 
5057   /**
5058    * Returns <code>true</code> if <code>wall</code> start or end point
5059    * equals the point (<code>x</code>, <code>y</code>).
5060    */
equalsWallPoint(float x, float y, Wall wall)5061   private boolean equalsWallPoint(float x, float y, Wall wall) {
5062     return x == wall.getXStart() && y == wall.getYStart()
5063            || x == wall.getXEnd() && y == wall.getYEnd();
5064   }
5065 
5066   /**
5067    * Paints room location feedback.
5068    */
paintRoomAlignmentFeedback(Graphics2D g2D, Room alignedRoom, Point2D locationFeedback, boolean showPointFeedback, Paint feedbackPaint, Stroke feedbackStroke, float planScale, Paint pointPaint, Stroke pointStroke)5069   private void paintRoomAlignmentFeedback(Graphics2D g2D,
5070                                           Room alignedRoom, Point2D locationFeedback,
5071                                           boolean showPointFeedback,
5072                                           Paint feedbackPaint, Stroke feedbackStroke,
5073                                           float planScale, Paint pointPaint,
5074                                           Stroke pointStroke) {
5075     // Paint room location feedback
5076     if (locationFeedback != null) {
5077       float margin = 0.5f / planScale;
5078       // Search which room points are at locationFeedback abscissa or ordinate
5079       float x = (float)locationFeedback.getX();
5080       float y = (float)locationFeedback.getY();
5081       float deltaXToClosestObject = Float.POSITIVE_INFINITY;
5082       float deltaYToClosestObject = Float.POSITIVE_INFINITY;
5083       for (Room room : getViewedItems(this.home.getRooms(), this.otherLevelsRoomsCache)) {
5084         float [][] roomPoints = room.getPoints();
5085         int editedPointIndex = -1;
5086         if (room == alignedRoom) {
5087           // Search which room point could match location feedback
5088           for (int i = 0; i < roomPoints.length; i++) {
5089             if (roomPoints [i][0] == x && roomPoints [i][1] == y) {
5090               editedPointIndex = i;
5091               break;
5092             }
5093           }
5094         }
5095         for (int i = 0; i < roomPoints.length; i++) {
5096           if (editedPointIndex == -1 || (i != editedPointIndex && roomPoints.length > 2)) {
5097             if (Math.abs(x - roomPoints [i][0]) < margin
5098                 && Math.abs(deltaYToClosestObject) > Math.abs(y - roomPoints [i][1])) {
5099               deltaYToClosestObject = y - roomPoints [i][1];
5100             }
5101             if (Math.abs(y - roomPoints [i][1]) < margin
5102                 && Math.abs(deltaXToClosestObject) > Math.abs(x - roomPoints [i][0])) {
5103               deltaXToClosestObject = x - roomPoints [i][0];
5104             }
5105           }
5106         }
5107       }
5108       // Search which wall points are at locationFeedback abscissa or ordinate
5109       for (Wall wall : getViewedItems(this.home.getWalls(), this.otherLevelsWallsCache)) {
5110         float [][] wallPoints = wall.getPoints();
5111         // Take into account only points at start and end of the wall
5112         wallPoints = new float [][] {wallPoints [0], wallPoints [wallPoints.length / 2 - 1],
5113                                      wallPoints [wallPoints.length / 2], wallPoints [wallPoints.length - 1]};
5114         for (int i = 0; i < wallPoints.length; i++) {
5115           if (Math.abs(x - wallPoints [i][0]) < margin
5116               && Math.abs(deltaYToClosestObject) > Math.abs(y - wallPoints [i][1])) {
5117             deltaYToClosestObject = y - wallPoints [i][1];
5118           }
5119           if (Math.abs(y - wallPoints [i][1]) < margin
5120               && Math.abs(deltaXToClosestObject) > Math.abs(x - wallPoints [i][0])) {
5121             deltaXToClosestObject = x - wallPoints [i][0];
5122           }
5123         }
5124       }
5125 
5126       // Draw alignment horizontal and vertical lines
5127       g2D.setPaint(feedbackPaint);
5128       g2D.setStroke(feedbackStroke);
5129       if (deltaXToClosestObject != Float.POSITIVE_INFINITY) {
5130         if (deltaXToClosestObject > 0) {
5131           g2D.draw(new Line2D.Float(x + ALIGNMENT_LINE_OFFSET / planScale, y,
5132               x - deltaXToClosestObject - ALIGNMENT_LINE_OFFSET / planScale, y));
5133         } else {
5134           g2D.draw(new Line2D.Float(x - ALIGNMENT_LINE_OFFSET / planScale, y,
5135               x - deltaXToClosestObject + ALIGNMENT_LINE_OFFSET / planScale, y));
5136         }
5137       }
5138 
5139       if (deltaYToClosestObject != Float.POSITIVE_INFINITY) {
5140         if (deltaYToClosestObject > 0) {
5141           g2D.draw(new Line2D.Float(x, y + ALIGNMENT_LINE_OFFSET / planScale,
5142               x, y - deltaYToClosestObject - ALIGNMENT_LINE_OFFSET / planScale));
5143         } else {
5144           g2D.draw(new Line2D.Float(x, y - ALIGNMENT_LINE_OFFSET / planScale,
5145               x, y - deltaYToClosestObject + ALIGNMENT_LINE_OFFSET / planScale));
5146         }
5147       }
5148 
5149       if (showPointFeedback) {
5150         paintPointFeedback(g2D, locationFeedback, feedbackPaint, planScale, pointPaint, pointStroke);
5151       }
5152     }
5153   }
5154 
5155   /**
5156    * Paints dimension line location feedback.
5157    */
paintDimensionLineAlignmentFeedback(Graphics2D g2D, DimensionLine alignedDimensionLine, Point2D locationFeedback, boolean showPointFeedback, Paint feedbackPaint, Stroke feedbackStroke, float planScale, Paint pointPaint, Stroke pointStroke)5158   private void paintDimensionLineAlignmentFeedback(Graphics2D g2D,
5159                                                    DimensionLine alignedDimensionLine, Point2D locationFeedback,
5160                                                    boolean showPointFeedback,
5161                                                    Paint feedbackPaint, Stroke feedbackStroke,
5162                                                    float planScale, Paint pointPaint,
5163                                                    Stroke pointStroke) {
5164     // Paint dimension line location feedback
5165     if (locationFeedback != null) {
5166       float margin = 0.5f / getScale();
5167       // Search which room points are at locationFeedback abscissa or ordinate
5168       float x = (float)locationFeedback.getX();
5169       float y = (float)locationFeedback.getY();
5170       float deltaXToClosestObject = Float.POSITIVE_INFINITY;
5171       float deltaYToClosestObject = Float.POSITIVE_INFINITY;
5172       for (Room room : getViewedItems(this.home.getRooms(), this.otherLevelsRoomsCache)) {
5173         float [][] roomPoints = room.getPoints();
5174         for (int i = 0; i < roomPoints.length; i++) {
5175           if (Math.abs(x - roomPoints [i][0]) < margin
5176               && Math.abs(deltaYToClosestObject) > Math.abs(y - roomPoints [i][1])) {
5177             deltaYToClosestObject = y - roomPoints [i][1];
5178           }
5179           if (Math.abs(y - roomPoints [i][1]) < margin
5180               && Math.abs(deltaXToClosestObject) > Math.abs(x - roomPoints [i][0])) {
5181             deltaXToClosestObject = x - roomPoints [i][0];
5182           }
5183         }
5184       }
5185       // Search which dimension line start or end point is at locationFeedback abscissa or ordinate
5186       // ignoring the start and end point of alignedDimensionLine
5187       for (DimensionLine dimensionLine : this.home.getDimensionLines()) {
5188         if (isViewableAtSelectedLevel(dimensionLine)
5189             && dimensionLine != alignedDimensionLine) {
5190           if (Math.abs(x - dimensionLine.getXStart()) < margin
5191               && (alignedDimensionLine == null
5192                   || !equalsDimensionLinePoint(dimensionLine.getXStart(), dimensionLine.getYStart(),
5193                           alignedDimensionLine))) {
5194             if (Math.abs(deltaYToClosestObject) > Math.abs(y - dimensionLine.getYStart())) {
5195               deltaYToClosestObject = y - dimensionLine.getYStart();
5196             }
5197           } else if (Math.abs(x - dimensionLine.getXEnd()) < margin
5198                     && (alignedDimensionLine == null
5199                         || !equalsDimensionLinePoint(dimensionLine.getXEnd(), dimensionLine.getYEnd(),
5200                                 alignedDimensionLine))) {
5201             if (Math.abs(deltaYToClosestObject) > Math.abs(y - dimensionLine.getYEnd())) {
5202               deltaYToClosestObject = y - dimensionLine.getYEnd();
5203             }
5204           }
5205           if (Math.abs(y - dimensionLine.getYStart()) < margin
5206               && (alignedDimensionLine == null
5207                   || !equalsDimensionLinePoint(dimensionLine.getXStart(), dimensionLine.getYStart(),
5208                           alignedDimensionLine))) {
5209             if (Math.abs(deltaXToClosestObject) > Math.abs(x - dimensionLine.getXStart())) {
5210               deltaXToClosestObject = x - dimensionLine.getXStart();
5211             }
5212           } else if (Math.abs(y - dimensionLine.getYEnd()) < margin
5213                     && (alignedDimensionLine == null
5214                         || !equalsDimensionLinePoint(dimensionLine.getXEnd(), dimensionLine.getYEnd(),
5215                                 alignedDimensionLine))) {
5216             if (Math.abs(deltaXToClosestObject) > Math.abs(x - dimensionLine.getXEnd())) {
5217               deltaXToClosestObject = x - dimensionLine.getXEnd();
5218             }
5219           }
5220         }
5221       }
5222       // Search which wall points are at locationFeedback abscissa or ordinate
5223       for (Wall wall : getViewedItems(this.home.getWalls(), this.otherLevelsWallsCache)) {
5224         float [][] wallPoints = wall.getPoints();
5225         // Take into account only points at start and end of the wall
5226         wallPoints = new float [][] {wallPoints [0], wallPoints [wallPoints.length / 2 - 1],
5227                                      wallPoints [wallPoints.length / 2], wallPoints [wallPoints.length - 1]};
5228         for (int i = 0; i < wallPoints.length; i++) {
5229           if (Math.abs(x - wallPoints [i][0]) < margin
5230               && Math.abs(deltaYToClosestObject) > Math.abs(y - wallPoints [i][1])) {
5231             deltaYToClosestObject = y - wallPoints [i][1];
5232           }
5233           if (Math.abs(y - wallPoints [i][1]) < margin
5234               && Math.abs(deltaXToClosestObject) > Math.abs(x - wallPoints [i][0])) {
5235             deltaXToClosestObject = x - wallPoints [i][0];
5236           }
5237         }
5238       }
5239       // Search which piece of furniture points are at locationFeedback abscissa or ordinate
5240       for (HomePieceOfFurniture piece : this.home.getFurniture()) {
5241         if (piece.isVisible()
5242             && isViewableAtSelectedLevel(piece)) {
5243           float [][] piecePoints = piece.getPoints();
5244           for (int i = 0; i < piecePoints.length; i++) {
5245             if (Math.abs(x - piecePoints [i][0]) < margin
5246                 && Math.abs(deltaYToClosestObject) > Math.abs(y - piecePoints [i][1])) {
5247               deltaYToClosestObject = y - piecePoints [i][1];
5248             }
5249             if (Math.abs(y - piecePoints [i][1]) < margin
5250                 && Math.abs(deltaXToClosestObject) > Math.abs(x - piecePoints [i][0])) {
5251               deltaXToClosestObject = x - piecePoints [i][0];
5252             }
5253 
5254           }
5255         }
5256       }
5257 
5258       // Draw alignment horizontal and vertical lines
5259       g2D.setPaint(feedbackPaint);
5260       g2D.setStroke(feedbackStroke);
5261       if (deltaXToClosestObject != Float.POSITIVE_INFINITY) {
5262         if (deltaXToClosestObject > 0) {
5263           g2D.draw(new Line2D.Float(x + ALIGNMENT_LINE_OFFSET / planScale, y,
5264               x - deltaXToClosestObject - ALIGNMENT_LINE_OFFSET / planScale, y));
5265         } else {
5266           g2D.draw(new Line2D.Float(x - ALIGNMENT_LINE_OFFSET / planScale, y,
5267               x - deltaXToClosestObject + ALIGNMENT_LINE_OFFSET / planScale, y));
5268         }
5269       }
5270 
5271       if (deltaYToClosestObject != Float.POSITIVE_INFINITY) {
5272         if (deltaYToClosestObject > 0) {
5273           g2D.draw(new Line2D.Float(x, y + ALIGNMENT_LINE_OFFSET / planScale,
5274               x, y - deltaYToClosestObject - ALIGNMENT_LINE_OFFSET / planScale));
5275         } else {
5276           g2D.draw(new Line2D.Float(x, y - ALIGNMENT_LINE_OFFSET / planScale,
5277               x, y - deltaYToClosestObject + ALIGNMENT_LINE_OFFSET / planScale));
5278         }
5279       }
5280 
5281       if (showPointFeedback) {
5282         paintPointFeedback(g2D, locationFeedback, feedbackPaint, planScale, pointPaint, pointStroke);
5283       }
5284     }
5285   }
5286 
5287   /**
5288    * Returns <code>true</code> if <code>dimensionLine</code> start or end point
5289    * equals the point (<code>x</code>, <code>y</code>).
5290    */
equalsDimensionLinePoint(float x, float y, DimensionLine dimensionLine)5291   private boolean equalsDimensionLinePoint(float x, float y, DimensionLine dimensionLine) {
5292     return x == dimensionLine.getXStart() && y == dimensionLine.getYStart()
5293            || x == dimensionLine.getXEnd() && y == dimensionLine.getYEnd();
5294   }
5295 
5296   /**
5297    * Paints an arc centered at <code>center</code> point that goes
5298    */
paintAngleFeedback(Graphics2D g2D, Point2D center, Point2D point1, Point2D point2, float planScale, Color selectionColor)5299   private void paintAngleFeedback(Graphics2D g2D, Point2D center,
5300                                   Point2D point1, Point2D point2,
5301                                   float planScale, Color selectionColor) {
5302     if (!point1.equals(center) && !point2.equals(center)) {
5303       g2D.setColor(selectionColor);
5304       g2D.setStroke(new BasicStroke(1 / planScale));
5305       // Compute angles
5306       double angle1 = Math.atan2(center.getY() - point1.getY(), point1.getX() - center.getX());
5307       if (angle1 < 0) {
5308         angle1 = 2 * Math.PI + angle1;
5309       }
5310       double angle2 = Math.atan2(center.getY() - point2.getY(), point2.getX() - center.getX());
5311       if (angle2 < 0) {
5312         angle2 = 2 * Math.PI + angle2;
5313       }
5314       double extent = angle2 - angle1;
5315       if (angle1 > angle2) {
5316         extent = 2 * Math.PI + extent;
5317       }
5318       AffineTransform previousTransform = g2D.getTransform();
5319       // Draw an arc
5320       g2D.translate(center.getX(), center.getY());
5321       float radius = 20 / planScale;
5322       g2D.draw(new Arc2D.Double(-radius, -radius,
5323           radius * 2, radius * 2, Math.toDegrees(angle1), Math.toDegrees(extent), Arc2D.OPEN));
5324       // Draw two radius
5325       radius += 5 / planScale;
5326       g2D.draw(new Line2D.Double(0, 0, radius * Math.cos(angle1), -radius * Math.sin(angle1)));
5327       g2D.draw(new Line2D.Double(0, 0, radius * Math.cos(angle1 + extent), -radius * Math.sin(angle1 + extent)));
5328       g2D.setTransform(previousTransform);
5329     }
5330   }
5331 
5332   /**
5333    * Paints the observer camera at its current location, if home camera is the observer camera.
5334    */
paintCamera(Graphics2D g2D, List<Selectable> selectedItems, Paint selectionOutlinePaint, Stroke selectionOutlineStroke, Paint indicatorPaint, float planScale, Color backgroundColor, Color foregroundColor)5335   private void paintCamera(Graphics2D g2D, List<Selectable> selectedItems,
5336                            Paint selectionOutlinePaint, Stroke selectionOutlineStroke,
5337                            Paint indicatorPaint, float planScale,
5338                            Color backgroundColor, Color foregroundColor) {
5339     ObserverCamera camera = this.home.getObserverCamera();
5340     if (camera == this.home.getCamera()) {
5341       AffineTransform previousTransform = g2D.getTransform();
5342       g2D.translate(camera.getX(), camera.getY());
5343       g2D.rotate(camera.getYaw());
5344 
5345       // Compute camera drawing at scale
5346       float [][] points = camera.getPoints();
5347       double yScale = Point2D.distance(points [0][0], points [0][1], points [3][0], points [3][1]);
5348       double xScale = Point2D.distance(points [0][0], points [0][1], points [1][0], points [1][1]);
5349       AffineTransform cameraTransform = AffineTransform.getScaleInstance(xScale, yScale);
5350       Shape scaledCameraBody =
5351           new Area(CAMERA_BODY).createTransformedArea(cameraTransform);
5352       Shape scaledCameraHead =
5353           new Area(CAMERA_HEAD).createTransformedArea(cameraTransform);
5354 
5355       // Paint body
5356       g2D.setPaint(backgroundColor);
5357       g2D.fill(scaledCameraBody);
5358       g2D.setPaint(foregroundColor);
5359       BasicStroke stroke = new BasicStroke(getStrokeWidth(ObserverCamera.class, PaintMode.PAINT) / planScale);
5360       g2D.setStroke(stroke);
5361       g2D.draw(scaledCameraBody);
5362 
5363       if (selectedItems.contains(camera)
5364           && this.selectedItemsOutlinePainted) {
5365         g2D.setPaint(selectionOutlinePaint);
5366         g2D.setStroke(selectionOutlineStroke);
5367         Area cameraOutline = new Area(scaledCameraBody);
5368         cameraOutline.add(new Area(scaledCameraHead));
5369         g2D.draw(cameraOutline);
5370       }
5371 
5372       // Paint head
5373       g2D.setPaint(backgroundColor);
5374       g2D.fill(scaledCameraHead);
5375       g2D.setPaint(foregroundColor);
5376       g2D.setStroke(stroke);
5377       g2D.draw(scaledCameraHead);
5378       // Paint field of sight angle
5379       double sin = (float)Math.sin(camera.getFieldOfView() / 2);
5380       double cos = (float)Math.cos(camera.getFieldOfView() / 2);
5381       float xStartAngle = (float)(0.9f * yScale * sin);
5382       float yStartAngle = (float)(0.9f * yScale * cos);
5383       float xEndAngle = (float)(2.2f * yScale * sin);
5384       float yEndAngle = (float)(2.2f * yScale * cos);
5385       GeneralPath cameraFieldOfViewAngle = new GeneralPath();
5386       cameraFieldOfViewAngle.moveTo(xStartAngle, yStartAngle);
5387       cameraFieldOfViewAngle.lineTo(xEndAngle, yEndAngle);
5388       cameraFieldOfViewAngle.moveTo(-xStartAngle, yStartAngle);
5389       cameraFieldOfViewAngle.lineTo(-xEndAngle, yEndAngle);
5390       g2D.draw(cameraFieldOfViewAngle);
5391       g2D.setTransform(previousTransform);
5392 
5393       // Paint resize indicator of selected camera
5394       if (selectedItems.size() == 1
5395           && selectedItems.get(0) == camera) {
5396         paintCameraRotationIndicators(g2D, camera, indicatorPaint, planScale);
5397       }
5398     }
5399   }
5400 
paintCameraRotationIndicators(Graphics2D g2D, ObserverCamera camera, Paint indicatorPaint, float planScale)5401   private void paintCameraRotationIndicators(Graphics2D g2D,
5402                                              ObserverCamera camera, Paint indicatorPaint,
5403                                              float planScale) {
5404     if (this.resizeIndicatorVisible) {
5405       g2D.setPaint(indicatorPaint);
5406       g2D.setStroke(INDICATOR_STROKE);
5407 
5408       AffineTransform previousTransform = g2D.getTransform();
5409       // Draw yaw rotation indicator at middle of first and last point of camera
5410       float [][] cameraPoints = camera.getPoints();
5411       float scaleInverse = 1 / planScale;
5412       g2D.translate((cameraPoints [0][0] + cameraPoints [3][0]) / 2,
5413           (cameraPoints [0][1] + cameraPoints [3][1]) / 2);
5414       g2D.scale(scaleInverse, scaleInverse);
5415       g2D.rotate(camera.getYaw());
5416       g2D.draw(getIndicator(camera, IndicatorType.ROTATE));
5417       g2D.setTransform(previousTransform);
5418 
5419       // Draw pitch rotation indicator at middle of second and third point of camera
5420       g2D.translate((cameraPoints [1][0] + cameraPoints [2][0]) / 2,
5421           (cameraPoints [1][1] + cameraPoints [2][1]) / 2);
5422       g2D.scale(scaleInverse, scaleInverse);
5423       g2D.rotate(camera.getYaw());
5424       g2D.draw(getIndicator(camera, IndicatorType.ROTATE_PITCH));
5425       g2D.setTransform(previousTransform);
5426 
5427       Shape elevationIndicator = getIndicator(camera, IndicatorType.ELEVATE);
5428       if (elevationIndicator != null) {
5429         // Draw elevation indicator at middle of first and second point of camera
5430         g2D.translate((cameraPoints [0][0] + cameraPoints [1][0]) / 2,
5431             (cameraPoints [0][1] + cameraPoints [1][1]) / 2);
5432         g2D.scale(scaleInverse, scaleInverse);
5433         g2D.draw(POINT_INDICATOR);
5434         g2D.translate(Math.sin(camera.getYaw()) * 8, -Math.cos(camera.getYaw()) * 8);
5435         g2D.draw(elevationIndicator);
5436         g2D.setTransform(previousTransform);
5437       }
5438     }
5439   }
5440 
5441   /**
5442    * Paints rectangle feedback.
5443    */
paintRectangleFeedback(Graphics2D g2D, Color selectionColor, float planScale)5444   private void paintRectangleFeedback(Graphics2D g2D, Color selectionColor, float planScale) {
5445     if (this.rectangleFeedback != null) {
5446       g2D.setPaint(new Color(selectionColor.getRed(), selectionColor.getGreen(), selectionColor.getBlue(), 32));
5447       g2D.fill(this.rectangleFeedback);
5448       g2D.setPaint(selectionColor);
5449       g2D.setStroke(new BasicStroke(1 / planScale));
5450       g2D.draw(this.rectangleFeedback);
5451     }
5452   }
5453 
5454   /**
5455    * Sets rectangle selection feedback coordinates.
5456    */
setRectangleFeedback(float x0, float y0, float x1, float y1)5457   public void setRectangleFeedback(float x0, float y0, float x1, float y1) {
5458     this.rectangleFeedback = new Rectangle2D.Float(x0, y0, 0, 0);
5459     this.rectangleFeedback.add(x1, y1);
5460     repaint();
5461   }
5462 
5463   /**
5464    * Ensures selected items are visible at screen and moves
5465    * scroll bars if needed.
5466    */
makeSelectionVisible()5467   public void makeSelectionVisible() {
5468     // As multiple selections may happen during an action,
5469     // make the selection visible the latest possible to avoid multiple changes
5470     if (!this.selectionScrollUpdated) {
5471       this.selectionScrollUpdated = true;
5472       EventQueue.invokeLater(new Runnable() {
5473           public void run() {
5474             selectionScrollUpdated = false;
5475             Rectangle2D selectionBounds = getSelectionBounds(true);
5476             if (selectionBounds != null) {
5477               Rectangle pixelBounds = getShapePixelBounds(selectionBounds);
5478               pixelBounds.grow(5, 5);
5479               Rectangle visibleRectangle = getVisibleRect();
5480               if (!pixelBounds.intersects(visibleRectangle)) {
5481                 scrollRectToVisible(pixelBounds);
5482               }
5483             }
5484           }
5485         });
5486     }
5487   }
5488 
5489   /**
5490    * Returns the bounds of the selected items.
5491    */
getSelectionBounds(boolean includeCamera)5492   private Rectangle2D getSelectionBounds(boolean includeCamera) {
5493     Graphics2D g = (Graphics2D)getGraphics();
5494     if (g != null) {
5495       setRenderingHints(g);
5496     }
5497     if (includeCamera) {
5498       return getItemsBounds(g, this.home.getSelectedItems());
5499     } else {
5500       List<Selectable> selectedItems = new ArrayList<Selectable>(this.home.getSelectedItems());
5501       selectedItems.remove(this.home.getCamera());
5502       return getItemsBounds(g, selectedItems);
5503     }
5504   }
5505 
5506   /**
5507    * Ensures the point at (<code>x</code>, <code>y</code>) is visible,
5508    * moving scroll bars if needed.
5509    */
makePointVisible(float x, float y)5510   public void makePointVisible(float x, float y) {
5511     scrollRectToVisible(getShapePixelBounds(
5512         new Rectangle2D.Float(x, y, getPixelLength(), getPixelLength())));
5513   }
5514 
5515   /**
5516    * Moves the view from (dx, dy) unit in the scrolling zone it belongs to.
5517    */
moveView(float dx, float dy)5518   public void moveView(float dx, float dy) {
5519     if (getParent() instanceof JViewport) {
5520       JViewport viewport = (JViewport)getParent();
5521       Rectangle viewRectangle = viewport.getViewRect();
5522       viewRectangle.translate(convertLengthToPixel(dx), convertLengthToPixel(dy));
5523       viewRectangle.x = Math.min(Math.max(0, viewRectangle.x), getWidth() - viewRectangle.width);
5524       viewRectangle.y = Math.min(Math.max(0, viewRectangle.y), getHeight() - viewRectangle.height);
5525       viewport.setViewPosition(viewRectangle.getLocation());
5526     }
5527   }
5528 
5529   /**
5530    * Returns the scale used to display the plan.
5531    */
getScale()5532   public float getScale() {
5533     return this.scale;
5534   }
5535 
5536   /**
5537    * Sets the scale used to display the plan.
5538    * If this component is displayed in a viewport the view position is updated
5539    * to ensure the center's view will remain the same after the scale change.
5540    */
setScale(float scale)5541   public void setScale(float scale) {
5542     if (this.scale != scale) {
5543       JViewport parent = null;
5544       Rectangle viewRectangle = null;
5545       float xViewCenterPosition = 0;
5546       float yViewCenterPosition = 0;
5547       if (getParent() instanceof JViewport) {
5548         parent = (JViewport)getParent();
5549         viewRectangle = parent.getViewRect();
5550         xViewCenterPosition = convertXPixelToModel(viewRectangle.x + viewRectangle.width / 2);
5551         yViewCenterPosition = convertYPixelToModel(viewRectangle.y + viewRectangle.height / 2);
5552       }
5553 
5554       this.scale = scale;
5555       // Revalidate plan without computing again unchanged plan bounds
5556       invalidate(false);
5557       revalidate();
5558 
5559       if (parent instanceof JViewport) {
5560         Dimension viewSize = parent.getViewSize();
5561         float viewWidth = convertPixelToLength(viewRectangle.width);
5562         int xViewLocation = Math.max(0, Math.min(convertXModelToPixel(xViewCenterPosition - viewWidth / 2),
5563             viewSize.width - viewRectangle.x));
5564         float viewHeight = convertPixelToLength(viewRectangle.height);
5565         int yViewLocation = Math.max(0, Math.min(convertYModelToPixel(yViewCenterPosition - viewHeight / 2),
5566             viewSize.height - viewRectangle.y));
5567         parent.setViewPosition(new Point(xViewLocation, yViewLocation));
5568       }
5569     }
5570   }
5571 
5572   /**
5573    * Returns <code>x</code> converted in model coordinates space.
5574    */
convertXPixelToModel(int x)5575   public float convertXPixelToModel(int x) {
5576     Insets insets = getInsets();
5577     Rectangle2D planBounds = getPlanBounds();
5578     return convertPixelToLength(x - insets.left) - MARGIN + (float)planBounds.getMinX();
5579   }
5580 
5581   /**
5582    * Returns <code>y</code> converted in model coordinates space.
5583    */
convertYPixelToModel(int y)5584   public float convertYPixelToModel(int y) {
5585     Insets insets = getInsets();
5586     Rectangle2D planBounds = getPlanBounds();
5587     return convertPixelToLength(y - insets.top) - MARGIN + (float)planBounds.getMinY();
5588   }
5589 
5590   /**
5591    * Returns the length in model units (cm) of the given <code>size</code> in pixels.
5592    */
convertPixelToLength(int size)5593   private float convertPixelToLength(int size) {
5594     return size * getPixelLength();
5595   }
5596 
5597   /**
5598    * Returns <code>x</code> converted in view coordinates space.
5599    */
convertXModelToPixel(float x)5600   private int convertXModelToPixel(float x) {
5601     Insets insets = getInsets();
5602     Rectangle2D planBounds = getPlanBounds();
5603     return convertLengthToPixel(x - planBounds.getMinX() + MARGIN) + insets.left;
5604   }
5605 
5606   /**
5607    * Returns <code>y</code> converted in view coordinates space.
5608    */
convertYModelToPixel(float y)5609   private int convertYModelToPixel(float y) {
5610     Insets insets = getInsets();
5611     Rectangle2D planBounds = getPlanBounds();
5612     return convertLengthToPixel(y - planBounds.getMinY() + MARGIN) + insets.top;
5613   }
5614 
5615   /**
5616    * Returns the size in pixels of the given <code>length</code> in model units (cm).
5617    */
convertLengthToPixel(double length)5618   private int convertLengthToPixel(double length) {
5619     return (int)Math.round(length / getPixelLength());
5620   }
5621 
5622   /**
5623    * Returns <code>x</code> converted in screen coordinates space.
5624    */
convertXModelToScreen(float x)5625   public int convertXModelToScreen(float x) {
5626     Point point = new Point(convertXModelToPixel(x), 0);
5627     SwingUtilities.convertPointToScreen(point, this);
5628     return point.x;
5629   }
5630 
5631   /**
5632    * Returns <code>y</code> converted in screen coordinates space.
5633    */
convertYModelToScreen(float y)5634   public int convertYModelToScreen(float y) {
5635     Point point = new Point(0, convertYModelToPixel(y));
5636     SwingUtilities.convertPointToScreen(point, this);
5637     return point.y;
5638   }
5639 
5640 
5641   /**
5642    * Returns the length in centimeters of a pixel with the current scale.
5643    */
getPixelLength()5644   public float getPixelLength() {
5645     return 1 / getScale() / this.resolutionScale;
5646   }
5647 
5648   /**
5649    * Returns the bounds of <code>shape</code> in pixels coordinates space.
5650    */
getShapePixelBounds(Shape shape)5651   private Rectangle getShapePixelBounds(Shape shape) {
5652     Rectangle2D shapeBounds = shape.getBounds2D();
5653     return new Rectangle(
5654         convertXModelToPixel((float)shapeBounds.getMinX()),
5655         convertYModelToPixel((float)shapeBounds.getMinY()),
5656         convertLengthToPixel(shapeBounds.getWidth()),
5657         convertLengthToPixel(shapeBounds.getHeight()));
5658   }
5659 
5660   /**
5661    * Sets the cursor of this component.
5662    */
setCursor(CursorType cursorType)5663   public void setCursor(CursorType cursorType) {
5664     switch (cursorType) {
5665       case DRAW :
5666         setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
5667         break;
5668       case ROTATION :
5669         setCursor(this.rotationCursor);
5670         break;
5671       case HEIGHT :
5672         setCursor(this.heightCursor);
5673         break;
5674       case POWER :
5675         setCursor(this.powerCursor);
5676         break;
5677       case ELEVATION :
5678         setCursor(this.elevationCursor);
5679         break;
5680       case RESIZE :
5681         setCursor(this.resizeCursor);
5682         break;
5683       case PANNING :
5684         setCursor(this.panningCursor);
5685         break;
5686       case DUPLICATION :
5687         setCursor(this.duplicationCursor);
5688         break;
5689       case MOVE :
5690         setCursor(this.moveCursor);
5691         break;
5692       case SELECTION :
5693       default :
5694         setCursor(Cursor.getDefaultCursor());
5695         break;
5696     }
5697   }
5698 
5699   /**
5700    * Sets tool tip text displayed as feedback.
5701    * @param toolTipFeedback the text displayed in the tool tip
5702    *                    or <code>null</code> to make tool tip disappear.
5703    */
setToolTipFeedback(String toolTipFeedback, float x, float y)5704   public void setToolTipFeedback(String toolTipFeedback, float x, float y) {
5705     stopToolTipPropertiesEdition();
5706     JToolTip toolTip = getToolTip();
5707     // Change tool tip text
5708     toolTip.setTipText(toolTipFeedback);
5709     showToolTipComponentAt(toolTip, x , y);
5710   }
5711 
5712   /**
5713    * Returns the tool tip of the plan.
5714    */
getToolTip()5715   private JToolTip getToolTip() {
5716     // Create tool tip for this component
5717     if (this.toolTip == null) {
5718       this.toolTip = new JToolTip();
5719       this.toolTip.setComponent(this);
5720     }
5721     return this.toolTip;
5722   }
5723 
5724   /**
5725    * Shows the given component as a tool tip.
5726    */
showToolTipComponentAt(JComponent toolTipComponent, float x, float y)5727   private void showToolTipComponentAt(JComponent toolTipComponent, float x, float y) {
5728     if (this.toolTipWindow == null) {
5729       // Show tool tip in a window (we don't use a Swing Popup because
5730       // we require the tool tip window to move along with mouse pointer
5731       // and a Swing popup can't move without hiding then showing it again)
5732       this.toolTipWindow = new JWindow(JOptionPane.getFrameForComponent(this));
5733       this.toolTipWindow.setFocusableWindowState(false);
5734       this.toolTipWindow.add(toolTipComponent);
5735       // Add to window a mouse listener that redispatch mouse events to
5736       // plan component (if the user moves fast enough the mouse pointer in a way
5737       // it's in toolTipWindow, the matching event is dispatched to toolTipWindow)
5738       MouseInputAdapter mouseAdapter = new MouseInputAdapter() {
5739           @Override
5740           public void mousePressed(MouseEvent ev) {
5741             mouseMoved(ev);
5742           }
5743 
5744           @Override
5745           public void mouseReleased(MouseEvent ev) {
5746             mouseMoved(ev);
5747           }
5748 
5749           @Override
5750           public void mouseMoved(MouseEvent ev) {
5751             dispatchEvent(SwingUtilities.convertMouseEvent(toolTipWindow, ev, PlanComponent.this));
5752           }
5753 
5754           @Override
5755           public void mouseDragged(MouseEvent ev) {
5756             mouseMoved(ev);
5757           }
5758         };
5759       this.toolTipWindow.addMouseListener(mouseAdapter);
5760       this.toolTipWindow.addMouseMotionListener(mouseAdapter);
5761     } else {
5762       Container contentPane = this.toolTipWindow.getContentPane();
5763       if (contentPane.getComponent(0) != toolTipComponent) {
5764         contentPane.removeAll();
5765         contentPane.add(toolTipComponent);
5766       }
5767       toolTipComponent.revalidate();
5768     }
5769     // Convert (x, y) to screen coordinates
5770     Point point = new Point(convertXModelToPixel(x), convertYModelToPixel(y));
5771     SwingUtilities.convertPointToScreen(point, this);
5772     // Add to point the half of cursor size
5773     Dimension cursorSize = getToolkit().getBestCursorSize(16, 16);
5774     if (cursorSize.width != 0) {
5775       point.x += cursorSize.width / 2 + 3;
5776       point.y += cursorSize.height / 2 + 3;
5777     } else {
5778       // If custom cursor isn't supported let's consider
5779       // default cursor size is 16 pixels wide
5780       point.x += 11;
5781       point.y += 11;
5782     }
5783     this.toolTipWindow.setLocation(point);
5784     this.toolTipWindow.pack();
5785     // Make the tooltip visible
5786     // (except in Applets run with Java 7 under Mac OS X where the tooltips are buggy)
5787     this.toolTipWindow.setVisible(!OperatingSystem.isMacOSX()
5788         || !OperatingSystem.isJavaVersionGreaterOrEqual("1.7")
5789         || SwingUtilities.getAncestorOfClass(JApplet.class, this) == null);
5790     toolTipComponent.paintImmediately(toolTipComponent.getBounds());
5791   }
5792 
5793   /**
5794    * Set tool tip edition.
5795    */
setToolTipEditedProperties(final PlanController.EditableProperty [] toolTipEditedProperties, Object [] toolTipPropertyValues, float x, float y)5796   public void setToolTipEditedProperties(final PlanController.EditableProperty [] toolTipEditedProperties,
5797                                          Object [] toolTipPropertyValues,
5798                                          float x, float y) {
5799     final JPanel toolTipPropertiesPanel = new JPanel(new GridBagLayout());
5800     // Reuse tool tip look
5801     Border border = UIManager.getBorder("ToolTip.border");
5802     if (!OperatingSystem.isMacOSX()
5803         || OperatingSystem.isMacOSXLeopardOrSuperior()) {
5804       border = BorderFactory.createCompoundBorder(border, BorderFactory.createEmptyBorder(0, 3, 0, 2));
5805     }
5806     toolTipPropertiesPanel.setBorder(border);
5807     // Copy colors from tool tip instance (on Linux, colors aren't set in UIManager)
5808     JToolTip toolTip = getToolTip();
5809     toolTipPropertiesPanel.setBackground(toolTip.getBackground());
5810     toolTipPropertiesPanel.setForeground(toolTip.getForeground());
5811 
5812     // Add labels and text fields to tool tip panel
5813     for (int i = 0; i < toolTipEditedProperties.length; i++) {
5814       JFormattedTextField textField = this.toolTipEditableTextFields.get(toolTipEditedProperties [i]);
5815       textField.setValue(toolTipPropertyValues [i]);
5816       JLabel label = new JLabel(this.preferences.getLocalizedString(PlanComponent.class,
5817           toolTipEditedProperties [i].name() + ".editablePropertyLabel.text") + " ");
5818       label.setFont(textField.getFont());
5819       JLabel unitLabel = null;
5820       if (toolTipEditedProperties [i] == PlanController.EditableProperty.ANGLE
5821           || toolTipEditedProperties [i] == PlanController.EditableProperty.ARC_EXTENT) {
5822         unitLabel = new JLabel(this.preferences.getLocalizedString(PlanComponent.class, "degreeLabel.text"));
5823       } else if (this.preferences.getLengthUnit() != LengthUnit.INCH
5824                  || this.preferences.getLengthUnit() != LengthUnit.INCH_DECIMALS) {
5825         unitLabel = new JLabel(" " + this.preferences.getLengthUnit().getName());
5826       }
5827 
5828       JPanel labelTextFieldPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0));
5829       labelTextFieldPanel.setOpaque(false);
5830 
5831       labelTextFieldPanel.add(label);
5832       labelTextFieldPanel.add(textField);
5833       if (unitLabel != null) {
5834         unitLabel.setFont(textField.getFont());
5835         labelTextFieldPanel.add(unitLabel);
5836       }
5837       toolTipPropertiesPanel.add(labelTextFieldPanel, new GridBagConstraints(
5838           0, i, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE,
5839           new Insets(0, 0, 0, 0), 0, 0));
5840     }
5841 
5842     showToolTipComponentAt(toolTipPropertiesPanel, x, y);
5843     // Add a key listener that redispatches events to tool tip text fields
5844     // (don't give focus to tool tip window otherwise plan component window will lose focus)
5845     this.toolTipKeyListener = new KeyListener() {
5846         private int focusedTextFieldIndex;
5847         private JFormattedTextField focusedTextField;
5848 
5849         {
5850           // Simulate focus on first text field
5851           setFocusedTextFieldIndex(0);
5852         }
5853 
5854         private void setFocusedTextFieldIndex(int textFieldIndex) {
5855           if (this.focusedTextField != null) {
5856             this.focusedTextField.getCaret().setVisible(false);
5857             this.focusedTextField.getCaret().setSelectionVisible(false);
5858             this.focusedTextField.setValue(this.focusedTextField.getValue());
5859           }
5860           this.focusedTextFieldIndex = textFieldIndex;
5861           this.focusedTextField = toolTipEditableTextFields.get(toolTipEditedProperties [textFieldIndex]);
5862           if (this.focusedTextField.getText().length() == 0) {
5863             this.focusedTextField.getCaret().setVisible(false);
5864           } else {
5865             this.focusedTextField.selectAll();
5866           }
5867           this.focusedTextField.getCaret().setSelectionVisible(true);
5868         }
5869 
5870         public void keyPressed(KeyEvent ev) {
5871           keyTyped(ev);
5872         }
5873 
5874         public void keyReleased(KeyEvent ev) {
5875           if (ev.getKeyCode() != KeyEvent.VK_CONTROL
5876               && ev.getKeyCode() != KeyEvent.VK_ALT) {
5877             // Forward other key events to focused text field (except for Ctrl and Alt key, otherwise InputMap won't receive it)
5878             KeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(this.focusedTextField, ev);
5879           }
5880         }
5881 
5882         public void keyTyped(KeyEvent ev) {
5883           Set<AWTKeyStroke> forwardKeys = this.focusedTextField.getFocusTraversalKeys(
5884               KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS);
5885           if (forwardKeys.contains(AWTKeyStroke.getAWTKeyStrokeForEvent(ev))
5886               || ev.getKeyCode() == KeyEvent.VK_DOWN) {
5887             setFocusedTextFieldIndex((this.focusedTextFieldIndex + 1) % toolTipEditedProperties.length);
5888             ev.consume();
5889           } else {
5890             Set<AWTKeyStroke> backwardKeys = this.focusedTextField.getFocusTraversalKeys(
5891                 KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS);
5892             if (backwardKeys.contains(AWTKeyStroke.getAWTKeyStrokeForEvent(ev))
5893                 || ev.getKeyCode() == KeyEvent.VK_UP) {
5894               setFocusedTextFieldIndex((this.focusedTextFieldIndex - 1 + toolTipEditedProperties.length) % toolTipEditedProperties.length);
5895               ev.consume();
5896             } else if ((ev.getKeyCode() == KeyEvent.VK_HOME
5897                           || ev.getKeyCode() == KeyEvent.VK_END)
5898                        && OperatingSystem.isMacOSX()
5899                        && !OperatingSystem.isMacOSXLeopardOrSuperior()) {
5900               // Support Home and End keys under Mac OS X Tiger
5901               if (ev.getKeyCode() == KeyEvent.VK_HOME) {
5902                 focusedTextField.setCaretPosition(0);
5903               } else if (ev.getKeyCode() == KeyEvent.VK_END) {
5904                 focusedTextField.setCaretPosition(focusedTextField.getText().length());
5905               }
5906               ev.consume();
5907             } else if (ev.getKeyCode() != KeyEvent.VK_ESCAPE
5908                        && ev.getKeyCode() != KeyEvent.VK_CONTROL
5909                        && ev.getKeyCode() != KeyEvent.VK_ALT) {
5910               // Forward other key events to focused text field (except for Esc key, otherwise InputMap won't receive it)
5911               KeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(this.focusedTextField, ev);
5912               this.focusedTextField.getCaret().setVisible(true);
5913               toolTipWindow.pack();
5914             }
5915           }
5916         }
5917       };
5918 
5919     addKeyListener(this.toolTipKeyListener);
5920     setFocusTraversalKeysEnabled(false);
5921     installEditionKeyboardActions();
5922   }
5923 
5924   /**
5925    * Deletes tool tip text from screen.
5926    */
deleteToolTipFeedback()5927   public void deleteToolTipFeedback() {
5928     stopToolTipPropertiesEdition();
5929     if (this.toolTip != null) {
5930       this.toolTip.setTipText(null);
5931     }
5932     if (this.toolTipWindow != null) {
5933       this.toolTipWindow.setVisible(false);
5934     }
5935   }
5936 
5937   /**
5938    * Stops editing in tool tip text fields.
5939    */
stopToolTipPropertiesEdition()5940   private void stopToolTipPropertiesEdition() {
5941     if (this.toolTipKeyListener != null) {
5942       installDefaultKeyboardActions();
5943       setFocusTraversalKeysEnabled(true);
5944       removeKeyListener(toolTipKeyListener);
5945       this.toolTipKeyListener = null;
5946 
5947       for (JFormattedTextField textField : this.toolTipEditableTextFields.values()) {
5948         textField.getCaret().setVisible(false);
5949         textField.getCaret().setSelectionVisible(false);
5950       }
5951     }
5952   }
5953 
5954   /**
5955    * Sets whether the resize indicator of selected wall or piece of furniture
5956    * should be visible or not.
5957    */
setResizeIndicatorVisible(boolean resizeIndicatorVisible)5958   public void setResizeIndicatorVisible(boolean resizeIndicatorVisible) {
5959     this.resizeIndicatorVisible = resizeIndicatorVisible;
5960     repaint();
5961   }
5962 
5963   /**
5964    * Sets the location point for alignment feedback.
5965    */
setAlignmentFeedback(Class<? extends Selectable> alignedObjectClass, Selectable alignedObject, float x, float y, boolean showPointFeedback)5966   public void setAlignmentFeedback(Class<? extends Selectable> alignedObjectClass,
5967                                    Selectable alignedObject,
5968                                    float x,
5969                                    float y,
5970                                    boolean showPointFeedback) {
5971     this.alignedObjectClass = alignedObjectClass;
5972     this.alignedObjectFeedback = alignedObject;
5973     this.locationFeeback = new Point2D.Float(x, y);
5974     this.showPointFeedback = showPointFeedback;
5975     repaint();
5976   }
5977 
5978   /**
5979    * Sets the points used to draw an angle in plan view.
5980    */
setAngleFeedback(float xCenter, float yCenter, float x1, float y1, float x2, float y2)5981   public void setAngleFeedback(float xCenter, float yCenter,
5982                                float x1, float y1,
5983                                float x2, float y2) {
5984     this.centerAngleFeedback = new Point2D.Float(xCenter, yCenter);
5985     this.point1AngleFeedback = new Point2D.Float(x1, y1);
5986     this.point2AngleFeedback = new Point2D.Float(x2, y2);
5987   }
5988 
5989   /**
5990    * Sets the feedback of dragged items drawn during a drag and drop operation,
5991    * initiated from outside of plan view.
5992    */
setDraggedItemsFeedback(List<Selectable> draggedItems)5993   public void setDraggedItemsFeedback(List<Selectable> draggedItems) {
5994     this.draggedItemsFeedback = draggedItems;
5995     repaint();
5996   }
5997 
5998   /**
5999    * Sets the given dimension lines to be drawn as feedback.
6000    */
setDimensionLinesFeedback(List<DimensionLine> dimensionLines)6001   public void setDimensionLinesFeedback(List<DimensionLine> dimensionLines) {
6002     this.dimensionLinesFeedback = dimensionLines;
6003     repaint();
6004   }
6005 
6006   /**
6007    * Deletes all elements shown as feedback.
6008    */
deleteFeedback()6009   public void deleteFeedback() {
6010     deleteToolTipFeedback();
6011     this.rectangleFeedback = null;
6012 
6013     this.alignedObjectClass = null;
6014     this.alignedObjectFeedback = null;
6015     this.locationFeeback = null;
6016 
6017     this.centerAngleFeedback = null;
6018     this.point1AngleFeedback = null;
6019     this.point2AngleFeedback = null;
6020 
6021     this.draggedItemsFeedback = null;
6022 
6023     this.dimensionLinesFeedback = null;
6024     repaint();
6025   }
6026 
6027   /**
6028    * Returns <code>true</code>.
6029    */
canImportDraggedItems(List<Selectable> items, int x, int y)6030   public boolean canImportDraggedItems(List<Selectable> items, int x, int y) {
6031     return true;
6032   }
6033 
6034   /**
6035    * Returns the size of the given piece of furniture in the horizontal plan,
6036    * or <code>null</code> if the view isn't able to compute such a value.
6037    */
getPieceOfFurnitureSizeInPlan(HomePieceOfFurniture piece)6038   public float [] getPieceOfFurnitureSizeInPlan(HomePieceOfFurniture piece) {
6039     if (piece.getRoll() == 0 && piece.getPitch() == 0) {
6040       return new float [] {piece.getWidth(), piece.getDepth(), piece.getHeight()};
6041     } else if (!isFurnitureSizeInPlanSupported()) {
6042       return null;
6043     } else {
6044       return PieceOfFurnitureModelIcon.computePieceOfFurnitureSizeInPlan(piece, this.object3dFactory);
6045     }
6046   }
6047 
6048   /**
6049    * Returns <code>true</code> if this component is able to compute the size of horizontally rotated furniture.
6050    */
isFurnitureSizeInPlanSupported()6051   public boolean isFurnitureSizeInPlanSupported() {
6052     try {
6053       return !Boolean.getBoolean("com.eteks.sweethome3d.no3D");
6054     } catch (AccessControlException ex) {
6055       // If com.eteks.sweethome3d.no3D can't be read,
6056       // security manager won't allow to access to Java 3D DLLs required by ModelManager class too
6057       return false;
6058     }
6059   }
6060 
6061   // Scrollable implementation
getPreferredScrollableViewportSize()6062   public Dimension getPreferredScrollableViewportSize() {
6063     return getPreferredSize();
6064   }
6065 
getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction)6066   public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
6067     if (orientation == SwingConstants.HORIZONTAL) {
6068       return visibleRect.width / 2;
6069     } else { // SwingConstants.VERTICAL
6070       return visibleRect.height / 2;
6071     }
6072   }
6073 
getScrollableTracksViewportHeight()6074   public boolean getScrollableTracksViewportHeight() {
6075     // Return true if the plan's preferred height is smaller than the viewport height
6076     return getParent() instanceof JViewport
6077         && getPreferredSize().height < ((JViewport)getParent()).getHeight();
6078   }
6079 
getScrollableTracksViewportWidth()6080   public boolean getScrollableTracksViewportWidth() {
6081     // Return true if the plan's preferred width is smaller than the viewport width
6082     return getParent() instanceof JViewport
6083         && getPreferredSize().width < ((JViewport)getParent()).getWidth();
6084   }
6085 
getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction)6086   public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
6087     if (orientation == SwingConstants.HORIZONTAL) {
6088       return visibleRect.width / 10;
6089     } else { // SwingConstants.VERTICAL
6090       return visibleRect.height / 10;
6091     }
6092   }
6093 
6094   /**
6095    * Returns the component used as an horizontal ruler for this plan.
6096    */
getHorizontalRuler()6097   public View getHorizontalRuler() {
6098     if (this.horizontalRuler == null) {
6099       this.horizontalRuler = new PlanRulerComponent(SwingConstants.HORIZONTAL);
6100     }
6101     return this.horizontalRuler;
6102   }
6103 
6104   /**
6105    * Returns the component used as a vertical ruler for this plan.
6106    */
getVerticalRuler()6107   public View getVerticalRuler() {
6108     if (this.verticalRuler == null) {
6109       this.verticalRuler = new PlanRulerComponent(SwingConstants.VERTICAL);
6110     }
6111     return this.verticalRuler;
6112   }
6113 
6114   /**
6115    * A component displaying the plan horizontal or vertical ruler associated to this plan.
6116    */
6117   public class PlanRulerComponent extends JComponent implements View {
6118     private int   orientation;
6119     private Point mouseLocation;
6120 
6121     /**
6122      * Creates a plan ruler.
6123      * @param orientation <code>SwingConstants.HORIZONTAL</code> or
6124      *                    <code>SwingConstants.VERTICAL</code>.
6125      */
PlanRulerComponent(int orientation)6126     public PlanRulerComponent(int orientation) {
6127       this.orientation = orientation;
6128       setOpaque(true);
6129       // Use same font as tool tips
6130       setFont(UIManager.getFont("ToolTip.font"));
6131       addMouseListeners();
6132     }
6133 
6134     /**
6135      * Adds a mouse listener to this ruler that stores current mouse location.
6136      */
addMouseListeners()6137     private void addMouseListeners() {
6138       MouseInputListener mouseInputListener = new MouseInputAdapter() {
6139           @Override
6140           public void mouseDragged(MouseEvent ev) {
6141             mouseLocation = ev.getPoint();
6142             repaint();
6143           }
6144 
6145           @Override
6146           public void mouseMoved(MouseEvent ev) {
6147             mouseLocation = ev.getPoint();
6148             repaint();
6149           }
6150 
6151           @Override
6152           public void mouseEntered(MouseEvent ev) {
6153             mouseLocation = ev.getPoint();
6154             repaint();
6155           }
6156 
6157           @Override
6158           public void mouseExited(MouseEvent ev) {
6159             mouseLocation = null;
6160             repaint();
6161           }
6162         };
6163       PlanComponent.this.addMouseListener(mouseInputListener);
6164       PlanComponent.this.addMouseMotionListener(mouseInputListener);
6165       addAncestorListener(new AncestorListener() {
6166           public void ancestorAdded(AncestorEvent ev) {
6167             removeAncestorListener(this);
6168             if (getParent() instanceof JViewport) {
6169               ((JViewport)getParent()).addChangeListener(new ChangeListener() {
6170                   public void stateChanged(ChangeEvent ev) {
6171                     mouseLocation = MouseInfo.getPointerInfo().getLocation();
6172                     SwingUtilities.convertPointFromScreen(mouseLocation, PlanComponent.this);
6173                     repaint();
6174                   }
6175                 });
6176             }
6177           }
6178 
6179           public void ancestorRemoved(AncestorEvent ev) {
6180           }
6181 
6182           public void ancestorMoved(AncestorEvent ev) {
6183           }
6184         });
6185     }
6186 
6187     /**
6188      * Returns the preferred size of this component.
6189      */
6190     @Override
getPreferredSize()6191     public Dimension getPreferredSize() {
6192       if (isPreferredSizeSet()) {
6193         return super.getPreferredSize();
6194       } else {
6195         Insets insets = getInsets();
6196         Rectangle2D planBounds = getPlanBounds();
6197         FontMetrics metrics = getFontMetrics(getFont());
6198         int ruleHeight = metrics.getAscent() + 6;
6199         if (this.orientation == SwingConstants.HORIZONTAL) {
6200           return new Dimension(
6201               convertLengthToPixel(planBounds.getWidth() + MARGIN * 2) + insets.left + insets.right,
6202               ruleHeight);
6203         } else {
6204           return new Dimension(ruleHeight,
6205               convertLengthToPixel(planBounds.getHeight() + MARGIN * 2) + insets.top + insets.bottom);
6206         }
6207       }
6208     }
6209 
6210     /**
6211      * Paints this component.
6212      */
6213     @Override
paintComponent(Graphics g)6214     protected void paintComponent(Graphics g) {
6215       Graphics2D g2D = (Graphics2D)g.create();
6216       paintBackground(g2D);
6217       Insets insets = getInsets();
6218       // Clip component to avoid drawing in empty borders
6219       g2D.clipRect(insets.left, insets.top,
6220           getWidth() - insets.left - insets.right,
6221           getHeight() - insets.top - insets.bottom);
6222       // Change component coordinates system to plan system
6223       Rectangle2D planBounds = getPlanBounds();
6224       float scale = getScale() * resolutionScale;
6225       g2D.translate(insets.left + (MARGIN - planBounds.getMinX()) * scale,
6226           insets.top + (MARGIN - planBounds.getMinY()) * scale);
6227       g2D.scale(scale, scale);
6228       g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
6229       g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
6230       // Paint component contents
6231       paintRuler(g2D, getScale());
6232       g2D.dispose();
6233     }
6234 
6235     /**
6236      * Fills the background with UI window background color.
6237      */
paintBackground(Graphics2D g2D)6238     private void paintBackground(Graphics2D g2D) {
6239       if (isOpaque()) {
6240         g2D.setColor(getBackground());
6241         g2D.fillRect(0, 0, getWidth(), getHeight());
6242       }
6243     }
6244 
6245     /**
6246      * Paints background grid lines.
6247      */
paintRuler(Graphics2D g2D, float rulerScale)6248     private void paintRuler(Graphics2D g2D, float rulerScale) {
6249       float gridSize = getGridSize(rulerScale);
6250       float mainGridSize = getMainGridSize(rulerScale);
6251 
6252       float xMin;
6253       float yMin;
6254       float xMax;
6255       float yMax;
6256       float xRulerBase;
6257       float yRulerBase;
6258       Rectangle2D planBounds = getPlanBounds();
6259       boolean leftToRightOriented = getComponentOrientation().isLeftToRight();
6260       if (getParent() instanceof JViewport) {
6261         Rectangle viewRectangle = ((JViewport)getParent()).getViewRect();
6262         xMin = convertXPixelToModel(viewRectangle.x - 1);
6263         yMin = convertYPixelToModel(viewRectangle.y - 1);
6264         xMax = convertXPixelToModel(viewRectangle.x + viewRectangle.width);
6265         yMax = convertYPixelToModel(viewRectangle.y + viewRectangle.height);
6266         xRulerBase = leftToRightOriented
6267             ? convertXPixelToModel(viewRectangle.x + viewRectangle.width - 1)
6268             : convertXPixelToModel(viewRectangle.x);
6269         yRulerBase = convertYPixelToModel(viewRectangle.y + viewRectangle.height - 1);
6270       } else {
6271         xMin = (float)planBounds.getMinX() - MARGIN;
6272         yMin = (float)planBounds.getMinY() - MARGIN;
6273         xMax = convertXPixelToModel(getWidth() - 1);
6274         yRulerBase =
6275         yMax = convertYPixelToModel(getHeight() - 1);
6276         xRulerBase = leftToRightOriented ? xMax : xMin;
6277       }
6278 
6279       FontMetrics metrics = getFontMetrics(getFont());
6280       float fontAscent = metrics.getAscent();
6281       float tickSize = 5 / rulerScale;
6282       float mainTickSize = (this.orientation == SwingConstants.HORIZONTAL ? getHeight() : getWidth()) * getPixelLength();
6283       NumberFormat format = NumberFormat.getIntegerInstance();
6284 
6285       g2D.setColor(getForeground());
6286       float lineWidth = 0.5f / rulerScale;
6287       g2D.setStroke(new BasicStroke(lineWidth));
6288       if (this.orientation == SwingConstants.HORIZONTAL) {
6289         // Draw horizontal ruler base
6290         g2D.draw(new Line2D.Float(xMin, yRulerBase - lineWidth / 2, xMax, yRulerBase - lineWidth  / 2));
6291         // Draw small ticks
6292         for (double x = (int)(xMin / gridSize) * gridSize; x < xMax; x += gridSize) {
6293           g2D.draw(new Line2D.Double(x, yMax - tickSize, x, yMax));
6294         }
6295       } else {
6296         // Draw vertical ruler base
6297         if (leftToRightOriented) {
6298           g2D.draw(new Line2D.Float(xRulerBase - lineWidth / 2, yMin, xRulerBase - lineWidth / 2, yMax));
6299         } else {
6300           g2D.draw(new Line2D.Float(xRulerBase + lineWidth / 2, yMin, xRulerBase + lineWidth / 2, yMax));
6301         }
6302         // Draw small ticks
6303         for (double y = (int)(yMin / gridSize) * gridSize; y < yMax; y += gridSize) {
6304           if (leftToRightOriented) {
6305             g2D.draw(new Line2D.Double(xMax - tickSize, y, xMax, y));
6306           } else {
6307             g2D.draw(new Line2D.Double(xMin, y, xMin + tickSize, y));
6308           }
6309         }
6310       }
6311 
6312       if (mainGridSize != gridSize) {
6313         g2D.setStroke(new BasicStroke(1.5f / rulerScale,
6314             BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
6315         AffineTransform previousTransform = g2D.getTransform();
6316         float scaleInverse = 1 / getScale() / resolutionScale;
6317         // Draw big ticks
6318         if (this.orientation == SwingConstants.HORIZONTAL) {
6319           for (double x = ((int)(xMin / mainGridSize) - 1) * mainGridSize; x < xMax; x += mainGridSize) {
6320             g2D.draw(new Line2D.Double(x, yMax - mainTickSize, x, yMax));
6321             // Draw unit text
6322             g2D.translate(x, yMax - mainTickSize);
6323             g2D.scale(scaleInverse, scaleInverse);
6324             g2D.drawString(getFormattedTickText(format, x), 3 * resolutionScale, fontAscent - resolutionScale * 2 + 1);
6325             g2D.setTransform(previousTransform);
6326           }
6327         } else {
6328           for (double y = ((int)(yMin / mainGridSize) - 1) * mainGridSize; y < yMax; y += mainGridSize) {
6329             String yText = getFormattedTickText(format, y);
6330             if (leftToRightOriented) {
6331               g2D.draw(new Line2D.Double(xMax - mainTickSize, y, xMax, y));
6332               // Draw unit text with a vertical orientation
6333               g2D.translate(xMax - mainTickSize, y);
6334               g2D.scale(scaleInverse, scaleInverse);
6335               g2D.rotate(-Math.PI / 2);
6336               g2D.drawString(yText, -metrics.stringWidth(yText) - 3 * resolutionScale, fontAscent - resolutionScale * 2 + 1);
6337             } else {
6338               g2D.draw(new Line2D.Double(xMin, y, xMin +  mainTickSize, y));
6339               // Draw unit text with a vertical orientation
6340               g2D.translate(xMin + mainTickSize, y);
6341               g2D.scale(scaleInverse, scaleInverse);
6342               g2D.rotate(Math.PI / 2);
6343               g2D.drawString(yText, 3 * resolutionScale, fontAscent - resolutionScale * 2 + 1);
6344             }
6345             g2D.setTransform(previousTransform);
6346           }
6347         }
6348       }
6349 
6350       if (this.mouseLocation != null) {
6351         g2D.setColor(getSelectionColor());
6352         g2D.setStroke(new BasicStroke(1 / rulerScale));
6353         if (this.orientation == SwingConstants.HORIZONTAL) {
6354           // Draw mouse feedback vertical line
6355           float x = convertXPixelToModel(this.mouseLocation.x);
6356           g2D.draw(new Line2D.Float(x, yMax - mainTickSize, x, yMax));
6357         } else {
6358           // Draw mouse feedback horizontal line
6359           float y = convertYPixelToModel(this.mouseLocation.y);
6360           if (leftToRightOriented) {
6361             g2D.draw(new Line2D.Float(xMax - mainTickSize, y, xMax, y));
6362           } else {
6363             g2D.draw(new Line2D.Float(xMin, y, xMin + mainTickSize, y));
6364           }
6365         }
6366       }
6367     }
6368 
getFormattedTickText(NumberFormat format, double value)6369     private String getFormattedTickText(NumberFormat format, double value) {
6370       String text;
6371       if (Math.abs(value) < 1E-5) {
6372         value = 0; // Avoid "-0" text
6373       }
6374       LengthUnit lengthUnit = preferences.getLengthUnit();
6375       if (lengthUnit == LengthUnit.INCH
6376           || lengthUnit == LengthUnit.INCH_DECIMALS) {
6377         text = format.format(LengthUnit.centimeterToFoot((float)value)) + "'";
6378       } else {
6379         text = format.format(value / 100);
6380         if (value == 0) {
6381           text += LengthUnit.METER.getName();
6382         }
6383       }
6384       return text;
6385     }
6386   }
6387 
6388   /**
6389    * A proxy for the furniture icon seen from top.
6390    */
6391   private abstract static class PieceOfFurnitureTopViewIcon implements Icon {
6392     private Icon icon;
6393 
PieceOfFurnitureTopViewIcon(Icon icon)6394     public PieceOfFurnitureTopViewIcon(Icon icon) {
6395       this.icon = icon;
6396     }
6397 
getIconWidth()6398     public int getIconWidth() {
6399       return this.icon.getIconWidth();
6400     }
6401 
getIconHeight()6402     public int getIconHeight() {
6403       return this.icon.getIconHeight();
6404     }
6405 
paintIcon(Component c, Graphics g, int x, int y)6406     public void paintIcon(Component c, Graphics g, int x, int y) {
6407       this.icon.paintIcon(c, g, x, y);
6408     }
6409 
isWaitIcon()6410     public boolean isWaitIcon() {
6411       return IconManager.getInstance().isWaitIcon(this.icon);
6412     }
6413 
isErrorIcon()6414     public boolean isErrorIcon() {
6415       return IconManager.getInstance().isErrorIcon(this.icon);
6416     }
6417 
setIcon(Icon icon)6418     protected void setIcon(Icon icon) {
6419       this.icon = icon;
6420     }
6421   }
6422 
6423   /**
6424    * A proxy for the furniture plan icon generated from its plan icon.
6425    */
6426   private static class PieceOfFurniturePlanIcon extends PieceOfFurnitureTopViewIcon {
6427     private final float pieceWidth;
6428     private final float pieceDepth;
6429     private Integer     pieceColor;
6430     private HomeTexture pieceTexture;
6431 
6432     /**
6433      * Creates a plan icon proxy for a <code>piece</code> of furniture.
6434      * @param piece an object containing a plan icon content
6435      * @param waitingComponent a waiting component. If <code>null</code>, the returned icon will
6436      *            be read immediately in the current thread.
6437      */
PieceOfFurniturePlanIcon(final HomePieceOfFurniture piece, final Component waitingComponent)6438     public PieceOfFurniturePlanIcon(final HomePieceOfFurniture piece,
6439                                     final Component waitingComponent) {
6440       super(IconManager.getInstance().getIcon(piece.getPlanIcon(), waitingComponent));
6441       this.pieceWidth = piece.getWidth();
6442       this.pieceDepth = piece.getDepth();
6443       this.pieceColor = piece.getColor();
6444       this.pieceTexture = piece.getTexture();
6445     }
6446 
6447     @Override
paintIcon(final Component c, Graphics g, int x, int y)6448     public void paintIcon(final Component c, Graphics g, int x, int y) {
6449       if (!isWaitIcon()
6450           && !isErrorIcon()) {
6451         if (this.pieceColor != null) {
6452           // Create a monochrome icon from plan icon
6453           BufferedImage image = new BufferedImage(getIconWidth(), getIconHeight(), BufferedImage.TYPE_INT_ARGB);
6454           Graphics imageGraphics = image.getGraphics();
6455           super.paintIcon(c, imageGraphics, 0, 0);
6456           imageGraphics.dispose();
6457 
6458           final int colorRed   = this.pieceColor & 0xFF0000;
6459           final int colorGreen = this.pieceColor & 0xFF00;
6460           final int colorBlue  = this.pieceColor & 0xFF;
6461           setIcon(new ImageIcon(c.createImage(new FilteredImageSource(image.getSource (),
6462               new RGBImageFilter() {
6463                 {
6464                   canFilterIndexColorModel = true;
6465                 }
6466 
6467                 public int filterRGB (int x, int y, int argb) {
6468                   int alpha = argb & 0xFF000000;
6469                   int red   = (argb & 0x00FF0000) >> 16;
6470                   int green = (argb & 0x0000FF00) >> 8;
6471                   int blue  = argb & 0x000000FF;
6472 
6473                   // Approximate brightness computation to 0.375 red + 0.5 green + 0.125 blue
6474                   // for faster results
6475                   int brightness = ((red + red + red + green + green + green + green + blue) >> 4) + 0x7F;
6476 
6477                   red   = (colorRed   * brightness / 0xFF) & 0xFF0000;
6478                   green = (colorGreen * brightness / 0xFF) & 0xFF00;
6479                   blue  = (colorBlue  * brightness / 0xFF) & 0xFF;
6480                   return alpha | red | green | blue;
6481                 }
6482               }))));
6483           // Don't need color information anymore
6484           this.pieceColor = null;
6485         } else if (this.pieceTexture != null) {
6486           if (isTextureManagerAvailable()) {
6487             // Prefer to share textures images with texture manager if it's available
6488             TextureManager.getInstance().loadTexture(this.pieceTexture.getImage(), true,
6489                 new TextureManager.TextureObserver() {
6490                   public void textureUpdated(Texture texture) {
6491                     setTexturedIcon(c, ((ImageComponent2D)texture.getImage(0)).getImage(), pieceTexture.getAngle());
6492                   }
6493                 });
6494           } else {
6495             Icon textureIcon = IconManager.getInstance().getIcon(this.pieceTexture.getImage(), null);
6496             if (IconManager.getInstance().isErrorIcon(textureIcon)) {
6497               setTexturedIcon(c, ERROR_TEXTURE_IMAGE, 0);
6498             } else {
6499               BufferedImage textureIconImage = new BufferedImage(
6500                   textureIcon.getIconWidth(), textureIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
6501               Graphics2D g2DIcon = (Graphics2D)textureIconImage.getGraphics();
6502               textureIcon.paintIcon(c, g2DIcon, 0, 0);
6503               g2DIcon.dispose();
6504               setTexturedIcon(c, textureIconImage, this.pieceTexture.getAngle());
6505             }
6506           }
6507 
6508           // Don't need texture information anymore
6509           this.pieceTexture = null;
6510         }
6511       }
6512       super.paintIcon(c, g, x, y);
6513     }
6514 
setTexturedIcon(Component c, BufferedImage textureImage, float angle)6515     private void setTexturedIcon(Component c, BufferedImage textureImage, float angle) {
6516       // Paint plan icon in an image
6517       BufferedImage image = new BufferedImage(getIconWidth(), getIconHeight(), BufferedImage.TYPE_INT_ARGB);
6518       final Graphics2D imageGraphics = (Graphics2D)image.getGraphics();
6519       imageGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
6520       PieceOfFurniturePlanIcon.super.paintIcon(c, imageGraphics, 0, 0);
6521 
6522       // Fill the pixels of plan icon with texture image
6523       imageGraphics.setPaint(new TexturePaint(textureImage,
6524           new Rectangle2D.Float(0, 0, -getIconWidth() / this.pieceWidth * this.pieceTexture.getWidth(),
6525               -getIconHeight() / this.pieceDepth * this.pieceTexture.getHeight())));
6526       imageGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN));
6527       imageGraphics.rotate(angle);
6528       float maxDimension = Math.max(image.getWidth(), image.getHeight());
6529       imageGraphics.fill(new Rectangle2D.Float(-maxDimension, -maxDimension, 3 * maxDimension, 3 * maxDimension));
6530       imageGraphics.fillRect(0, 0, getIconWidth(), getIconHeight());
6531       imageGraphics.dispose();
6532 
6533       setIcon(new ImageIcon(image));
6534     }
6535   }
6536 
6537   /**
6538    * A proxy for the furniture top view icon generated from its 3D model.
6539    */
6540   private static class PieceOfFurnitureModelIcon extends PieceOfFurnitureTopViewIcon {
6541     private static BranchGroup     sceneRoot;
6542     private static ExecutorService iconsCreationExecutor;
6543 
6544     /**
6545      * Creates a top view icon proxy for a <code>piece</code> of furniture.
6546      * @param piece an object containing a 3D content
6547      * @param waitingComponent a waiting component. If <code>null</code>, the returned icon will
6548      *            be read immediately in the current thread.
6549      * @param iconSize the size in pixels of the generated icon
6550      */
PieceOfFurnitureModelIcon(final HomePieceOfFurniture piece, final Object3DFactory object3dFactory, final Component waitingComponent, final int iconSize)6551     public PieceOfFurnitureModelIcon(final HomePieceOfFurniture piece,
6552                                      final Object3DFactory object3dFactory,
6553                                      final Component waitingComponent,
6554                                      final int iconSize) {
6555       super(IconManager.getInstance().getWaitIcon());
6556       ModelManager.getInstance().loadModel(piece.getModel(), waitingComponent == null,
6557           new ModelManager.ModelObserver() {
6558             public void modelUpdated(final BranchGroup modelNode) {
6559               // Now that it's sure that 3D model exists
6560               // work on a clone of the piece centered at the origin
6561               // with the same size to get a correct texture mapping
6562               final HomePieceOfFurniture normalizedPiece = piece.clone();
6563               if (normalizedPiece.isResizable()
6564                   && piece.getRoll() == 0) {
6565                 normalizedPiece.setModelMirrored(false);
6566               }
6567               final float pieceWidth = normalizedPiece.getWidthInPlan();
6568               final float pieceDepth = normalizedPiece.getDepthInPlan();
6569               final float pieceHeight = normalizedPiece.getHeightInPlan();
6570               normalizedPiece.setX(0);
6571               normalizedPiece.setY(0);
6572               normalizedPiece.setElevation(-pieceHeight / 2);
6573               normalizedPiece.setLevel(null);
6574               normalizedPiece.setAngle(0);
6575               if (waitingComponent != null) {
6576                 // Generate icons in an other thread to avoid blocking EDT during offscreen rendering
6577                 if (iconsCreationExecutor == null) {
6578                   iconsCreationExecutor = Executors.newSingleThreadExecutor();
6579                 }
6580                 iconsCreationExecutor.execute(new Runnable() {
6581                     public void run() {
6582                       setIcon(createIcon((Object3DBranch)object3dFactory.createObject3D(null, normalizedPiece, true),
6583                           pieceWidth, pieceDepth, pieceHeight, iconSize));
6584                       waitingComponent.repaint();
6585                     }
6586                   });
6587               } else {
6588                 setIcon(createIcon((Object3DBranch)object3dFactory.createObject3D(null, normalizedPiece, true),
6589                     pieceWidth, pieceDepth, pieceHeight, iconSize));
6590               }
6591             }
6592 
6593             public void modelError(Exception ex) {
6594               // Too bad, we'll use errorIcon
6595               setIcon(IconManager.getInstance().getErrorIcon());
6596               if (waitingComponent != null) {
6597                 waitingComponent.repaint();
6598               }
6599             }
6600           });
6601     }
6602 
6603     /**
6604      * Returns the branch group bound to a universe and a canvas for the given resolution.
6605      */
getSceneRoot(int iconSize)6606     private BranchGroup getSceneRoot(int iconSize) {
6607       if (sceneRoot == null) {
6608         // Create the universe used to compute top view icons
6609         Canvas3D canvas3D = Component3DManager.getInstance().getOffScreenCanvas3D(iconSize, iconSize);
6610         SimpleUniverse universe = new SimpleUniverse(canvas3D);
6611         ViewingPlatform viewingPlatform = universe.getViewingPlatform();
6612         // View model from top
6613         TransformGroup viewPlatformTransform = viewingPlatform.getViewPlatformTransform();
6614         Transform3D rotation = new Transform3D();
6615         rotation.rotX(-Math.PI / 2);
6616         viewPlatformTransform.setTransform(rotation);
6617         // Use parallel projection with a frustrum front and back distance
6618         // limited to an object centered at the origin and contained in a 2 units wide cube
6619         Viewer viewer = viewingPlatform.getViewers() [0];
6620         javax.media.j3d.View view = viewer.getView();
6621         view.setProjectionPolicy(javax.media.j3d.View.PARALLEL_PROJECTION);
6622         view.setFrontClipDistance(-1.1f);
6623         view.setBackClipDistance(1.1f);
6624         sceneRoot = new BranchGroup();
6625         // Prepare scene root
6626         sceneRoot.setCapability(BranchGroup.ALLOW_CHILDREN_READ);
6627         sceneRoot.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
6628         sceneRoot.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
6629         Background background = new Background(1.1f, 1.1f, 1.1f);
6630         background.setCapability(Background.ALLOW_COLOR_WRITE);
6631         background.setApplicationBounds(new BoundingBox(new Point3d(-1.1, -1.1, -1.1), new Point3d(1.1, 1.1, 1.1)));
6632         sceneRoot.addChild(background);
6633         Light [] lights = {new DirectionalLight(new Color3f(0.6f, 0.6f, 0.6f), new Vector3f(1.5f, -0.8f, -1)),
6634                            new DirectionalLight(new Color3f(0.6f, 0.6f, 0.6f), new Vector3f(-1.5f, -0.8f, -1)),
6635                            new DirectionalLight(new Color3f(0.6f, 0.6f, 0.6f), new Vector3f(0, -0.8f, 1)),
6636                            new AmbientLight(new Color3f(0.2f, 0.2f, 0.2f))};
6637         for (Light light : lights) {
6638           light.setInfluencingBounds(new BoundingBox(new Point3d(-1.1, -1.1, -1.1), new Point3d(1.1, 1.1, 1.1)));
6639           sceneRoot.addChild(light);
6640         }
6641         universe.addBranchGraph(sceneRoot);
6642         sceneRoot.setUserData(universe); // Store universe in user data to be able to access it under Java 3D 1.3
6643       } else {
6644         SimpleUniverse universe = (SimpleUniverse)sceneRoot.getUserData();
6645         Canvas3D canvas3D = universe.getCanvas();
6646         if (canvas3D.getWidth() != iconSize) {
6647           universe.cleanup();
6648           sceneRoot = null;
6649           return getSceneRoot(iconSize);
6650         }
6651       }
6652       return sceneRoot;
6653     }
6654 
6655     /**
6656      * Returns an icon created and scaled from piece model content.
6657      */
createIcon(Object3DBranch pieceNode, float pieceWidth, float pieceDepth, float pieceHeight, int iconSize)6658     private Icon createIcon(Object3DBranch pieceNode,
6659                             float pieceWidth, float pieceDepth, float pieceHeight,
6660                             int iconSize) {
6661       // Add piece model scene to a normalized transform group
6662       Transform3D scaleTransform = new Transform3D();
6663       scaleTransform.setScale(new Vector3d(2 / pieceWidth, 2 / pieceHeight, 2 / pieceDepth));
6664       TransformGroup modelTransformGroup = new TransformGroup();
6665       modelTransformGroup.setTransform(scaleTransform);
6666       modelTransformGroup.addChild(pieceNode);
6667       // Replace model textures by clones because Java 3D doesn't accept all the time
6668       // to share textures between offscreen and onscreen environments
6669       cloneTexture(pieceNode, new IdentityHashMap<Texture, Texture>());
6670 
6671       BranchGroup model = new BranchGroup();
6672       model.setCapability(BranchGroup.ALLOW_DETACH);
6673       model.addChild(modelTransformGroup);
6674       BranchGroup sceneRoot = getSceneRoot(iconSize);
6675       sceneRoot.addChild(model);
6676 
6677       // Render scene with a white background
6678       Background background = (Background)sceneRoot.getChild(0);
6679       background.setColor(1, 1, 1);
6680       Canvas3D canvas3D = ((SimpleUniverse)sceneRoot.getUserData()).getCanvas();
6681       canvas3D.renderOffScreenBuffer();
6682       canvas3D.waitForOffScreenRendering();
6683       BufferedImage imageWithWhiteBackgound = canvas3D.getOffScreenBuffer().getImage();
6684       int [] imageWithWhiteBackgoundPixels = getImagePixels(imageWithWhiteBackgound);
6685 
6686       // Render scene with a black background
6687       background.setColor(0, 0, 0);
6688       canvas3D.renderOffScreenBuffer();
6689       canvas3D.waitForOffScreenRendering();
6690       BufferedImage imageWithBlackBackgound = canvas3D.getOffScreenBuffer().getImage();
6691       int [] imageWithBlackBackgoundPixels = getImagePixels(imageWithBlackBackgound);
6692 
6693       // Create an image with transparent pixels where model isn't drawn
6694       for (int i = 0; i < imageWithBlackBackgoundPixels.length; i++) {
6695         if (imageWithBlackBackgoundPixels [i] != imageWithWhiteBackgoundPixels [i]
6696             && imageWithBlackBackgoundPixels [i] == 0xFF000000
6697             && imageWithWhiteBackgoundPixels [i] == 0xFFFFFFFF) {
6698           imageWithWhiteBackgoundPixels [i] = 0;
6699         }
6700       }
6701 
6702       sceneRoot.removeChild(model);
6703       return new ImageIcon(Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(
6704           imageWithWhiteBackgound.getWidth(), imageWithWhiteBackgound.getHeight(),
6705           imageWithWhiteBackgoundPixels, 0, imageWithWhiteBackgound.getWidth())));
6706     }
6707 
6708     /**
6709      * Replace the textures set on node shapes by clones.
6710      */
cloneTexture(Node node, Map<Texture, Texture> replacedTextures)6711     private void cloneTexture(Node node, Map<Texture, Texture> replacedTextures) {
6712       if (node instanceof Group) {
6713         // Enumerate children
6714         Enumeration<?> enumeration = ((Group)node).getAllChildren();
6715         while (enumeration.hasMoreElements()) {
6716           cloneTexture((Node)enumeration.nextElement(), replacedTextures);
6717         }
6718       } else if (node instanceof Link) {
6719         cloneTexture(((Link)node).getSharedGroup(), replacedTextures);
6720       } else if (node instanceof Shape3D) {
6721         Appearance appearance = ((Shape3D)node).getAppearance();
6722         if (appearance != null) {
6723           Texture texture = appearance.getTexture();
6724           if (texture != null) {
6725             Texture replacedTexture = replacedTextures.get(texture);
6726             if (replacedTexture == null) {
6727               replacedTexture = (Texture)texture.cloneNodeComponent(false);
6728               replacedTextures.put(texture, replacedTexture);
6729             }
6730             appearance.setTexture(replacedTexture);
6731           }
6732         }
6733       }
6734     }
6735 
6736     /**
6737      * Returns the pixels of the given <code>image</code>.
6738      */
getImagePixels(BufferedImage image)6739     private int [] getImagePixels(BufferedImage image) {
6740       if (image.getType() == BufferedImage.TYPE_INT_RGB
6741           || image.getType() == BufferedImage.TYPE_INT_ARGB) {
6742         // Use a faster way to get pixels
6743         return (int [])image.getRaster().getDataElements(0, 0, image.getWidth(), image.getHeight(), null);
6744       } else {
6745         return image.getRGB(0, 0, image.getWidth(), image.getHeight(), null,
6746             0, image.getWidth());
6747       }
6748     }
6749 
6750     /**
6751      * Returns the size of the given piece computed from its vertices.
6752      */
computePieceOfFurnitureSizeInPlan(HomePieceOfFurniture piece, Object3DFactory object3dFactory)6753     private static float [] computePieceOfFurnitureSizeInPlan(HomePieceOfFurniture piece,
6754                                                               Object3DFactory object3dFactory) {
6755       Transform3D horizontalRotation = new Transform3D();
6756       // Change its angles around horizontal axes
6757       if (piece.getPitch() != 0) {
6758         horizontalRotation.rotX(-piece.getPitch());
6759       }
6760       if (piece.getRoll() != 0) {
6761         Transform3D rollRotation = new Transform3D();
6762         rollRotation.rotZ(-piece.getRoll());
6763         horizontalRotation.mul(rollRotation, horizontalRotation);
6764       }
6765 
6766       // Compute bounds of a piece centered at the origin and rotated around the target horizontal angle
6767       piece = piece.clone();
6768       piece.setX(0);
6769       piece.setY(0);
6770       piece.setElevation(-piece.getHeight() / 2);
6771       piece.setLevel(null);
6772       piece.setAngle(0);
6773       piece.setRoll(0);
6774       piece.setPitch(0);
6775       piece.setWidthInPlan(piece.getWidth());
6776       piece.setDepthInPlan(piece.getDepth());
6777       piece.setHeightInPlan(piece.getHeight());
6778       BoundingBox bounds = ModelManager.getInstance().getBounds(
6779           (Object3DBranch)object3dFactory.createObject3D(null, piece, true), horizontalRotation);
6780       Point3d lower = new Point3d();
6781       bounds.getLower(lower);
6782       Point3d upper = new Point3d();
6783       bounds.getUpper(upper);
6784       return new float [] {
6785           Math.max(0.001f, (float)(upper.x - lower.x)), // width in plan
6786           Math.max(0.001f, (float)(upper.z - lower.z)), // depth in plan
6787           Math.max(0.001f, (float)(upper.y - lower.y))}; // height in plan
6788     }
6789   }
6790 
6791   /**
6792    * A map key used to compare furniture with the same top view icon.
6793    */
6794   private static class HomePieceOfFurnitureTopViewIconKey {
6795     private HomePieceOfFurniture piece;
6796     private int                  hashCode;
6797 
HomePieceOfFurnitureTopViewIconKey(HomePieceOfFurniture piece)6798     public HomePieceOfFurnitureTopViewIconKey(HomePieceOfFurniture piece) {
6799       this.piece = piece;
6800       this.hashCode = (piece.getPlanIcon() != null ? piece.getPlanIcon().hashCode() : piece.getModel().hashCode())
6801           + (piece.getColor() != null ? 37 * piece.getColor().hashCode() : 1234);
6802       if (piece.isHorizontallyRotated()
6803           || piece.getTexture() != null) {
6804         this.hashCode +=
6805               (piece.getTexture() != null ? 37 * piece.getTexture().hashCode() : 0)
6806             + 37 * Float.valueOf(piece.getWidthInPlan()).hashCode()
6807             + 37 * Float.valueOf(piece.getDepthInPlan()).hashCode()
6808             + 37 * Float.valueOf(piece.getHeightInPlan()).hashCode();
6809       }
6810       if (piece.getRoll() != 0) {
6811         this.hashCode += 37 * Boolean.valueOf(piece.isModelMirrored()).hashCode();
6812       }
6813       if (piece.getPlanIcon() != null) {
6814         this.hashCode +=
6815               37 * Arrays.deepHashCode(piece.getModelRotation())
6816             + 37 * Boolean.valueOf(piece.isModelCenteredAtOrigin()).hashCode()
6817             + 37 * Boolean.valueOf(piece.isBackFaceShown()).hashCode()
6818             + 37 * Float.valueOf(piece.getPitch()).hashCode()
6819             + 37 * Float.valueOf(piece.getRoll()).hashCode()
6820             + 37 * Arrays.hashCode(piece.getModelTransformations())
6821             + 37 * Arrays.hashCode(piece.getModelMaterials())
6822             + (piece.getShininess() != null ? 37 * piece.getShininess().hashCode() : 3456);
6823       }
6824     }
6825 
6826     @Override
equals(Object obj)6827     public boolean equals(Object obj) {
6828       if (obj instanceof HomePieceOfFurnitureTopViewIconKey) {
6829         HomePieceOfFurniture piece2 = ((HomePieceOfFurnitureTopViewIconKey)obj).piece;
6830         // Test all furniture data that could make change the plan icon
6831         // (see HomePieceOfFurniture3D and PlanComponent#addModelListeners for changes conditions)
6832         return (this.piece.getPlanIcon() != null
6833                   ? this.piece.getPlanIcon().equals(piece2.getPlanIcon())
6834                   : this.piece.getModel().equals(piece2.getModel()))
6835             && (this.piece.getColor() == piece2.getColor()
6836                 || this.piece.getColor() != null && this.piece.getColor().equals(piece2.getColor()))
6837             && (this.piece.getTexture() == piece2.getTexture()
6838                 || this.piece.getTexture() != null && this.piece.getTexture().equals(piece2.getTexture()))
6839             && (!this.piece.isHorizontallyRotated()
6840                     && !piece2.isHorizontallyRotated()
6841                     && this.piece.getTexture() == null
6842                     && piece2.getTexture() == null
6843                 || this.piece.getWidthInPlan() == piece2.getWidthInPlan()
6844                     && this.piece.getDepthInPlan() == piece2.getDepthInPlan()
6845                     && this.piece.getHeightInPlan() == piece2.getHeightInPlan())
6846             && (this.piece.getRoll() == 0
6847                 && piece2.getRoll() == 0
6848                 || this.piece.isModelMirrored() == piece2.isModelMirrored())
6849             && (this.piece.getPlanIcon() != null
6850                 || Arrays.deepEquals(this.piece.getModelRotation(), piece2.getModelRotation())
6851                     && this.piece.isModelCenteredAtOrigin() == piece2.isModelCenteredAtOrigin()
6852                     && this.piece.isBackFaceShown() == piece2.isBackFaceShown()
6853                     && this.piece.getPitch() == piece2.getPitch()
6854                     && this.piece.getRoll() == piece2.getRoll()
6855                     && Arrays.equals(this.piece.getModelTransformations(), piece2.getModelTransformations())
6856                     && Arrays.equals(this.piece.getModelMaterials(), piece2.getModelMaterials())
6857                     && (this.piece.getShininess() == piece2.getShininess()
6858                         || this.piece.getShininess() != null && this.piece.getShininess().equals(piece2.getShininess())));
6859       } else {
6860         return false;
6861       }
6862     }
6863 
6864     @Override
hashCode()6865     public int hashCode() {
6866       return this.hashCode;
6867     }
6868   }
6869 }
6870