1 /*
2  * HomeComponent3D.java 24 ao?t 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.AlphaComposite;
23 import java.awt.Color;
24 import java.awt.Component;
25 import java.awt.ComponentOrientation;
26 import java.awt.Composite;
27 import java.awt.Container;
28 import java.awt.Dimension;
29 import java.awt.EventQueue;
30 import java.awt.Graphics;
31 import java.awt.Graphics2D;
32 import java.awt.GraphicsConfiguration;
33 import java.awt.GraphicsEnvironment;
34 import java.awt.GridBagConstraints;
35 import java.awt.GridBagLayout;
36 import java.awt.GridLayout;
37 import java.awt.Insets;
38 import java.awt.LayoutManager;
39 import java.awt.Point;
40 import java.awt.Rectangle;
41 import java.awt.RenderingHints;
42 import java.awt.event.ActionEvent;
43 import java.awt.event.ActionListener;
44 import java.awt.event.ComponentAdapter;
45 import java.awt.event.ComponentEvent;
46 import java.awt.event.ComponentListener;
47 import java.awt.event.MouseAdapter;
48 import java.awt.event.MouseEvent;
49 import java.awt.event.MouseListener;
50 import java.awt.event.MouseMotionAdapter;
51 import java.awt.event.MouseMotionListener;
52 import java.awt.event.MouseWheelEvent;
53 import java.awt.event.MouseWheelListener;
54 import java.awt.geom.Area;
55 import java.awt.geom.GeneralPath;
56 import java.awt.geom.PathIterator;
57 import java.awt.geom.Rectangle2D;
58 import java.awt.image.BufferedImage;
59 import java.awt.image.FilteredImageSource;
60 import java.awt.image.RGBImageFilter;
61 import java.awt.print.PageFormat;
62 import java.awt.print.Printable;
63 import java.beans.PropertyChangeEvent;
64 import java.beans.PropertyChangeListener;
65 import java.lang.ref.WeakReference;
66 import java.lang.reflect.Field;
67 import java.util.ArrayList;
68 import java.util.Arrays;
69 import java.util.Collection;
70 import java.util.Collections;
71 import java.util.Comparator;
72 import java.util.Enumeration;
73 import java.util.HashMap;
74 import java.util.HashSet;
75 import java.util.List;
76 import java.util.Map;
77 import java.util.Set;
78 import java.util.TreeMap;
79 import java.util.concurrent.Executors;
80 import java.util.concurrent.ScheduledExecutorService;
81 import java.util.concurrent.TimeUnit;
82 
83 import javax.media.j3d.Alpha;
84 import javax.media.j3d.AmbientLight;
85 import javax.media.j3d.Appearance;
86 import javax.media.j3d.Background;
87 import javax.media.j3d.BoundingBox;
88 import javax.media.j3d.BoundingSphere;
89 import javax.media.j3d.Bounds;
90 import javax.media.j3d.BranchGroup;
91 import javax.media.j3d.Canvas3D;
92 import javax.media.j3d.ColoringAttributes;
93 import javax.media.j3d.DirectionalLight;
94 import javax.media.j3d.Geometry;
95 import javax.media.j3d.GraphicsConfigTemplate3D;
96 import javax.media.j3d.Group;
97 import javax.media.j3d.IllegalRenderingStateException;
98 import javax.media.j3d.J3DGraphics2D;
99 import javax.media.j3d.Light;
100 import javax.media.j3d.Link;
101 import javax.media.j3d.Material;
102 import javax.media.j3d.Node;
103 import javax.media.j3d.PointArray;
104 import javax.media.j3d.RenderingAttributes;
105 import javax.media.j3d.Shape3D;
106 import javax.media.j3d.TexCoordGeneration;
107 import javax.media.j3d.Texture;
108 import javax.media.j3d.TextureAttributes;
109 import javax.media.j3d.Transform3D;
110 import javax.media.j3d.TransformGroup;
111 import javax.media.j3d.TransformInterpolator;
112 import javax.media.j3d.TransparencyAttributes;
113 import javax.media.j3d.View;
114 import javax.media.j3d.VirtualUniverse;
115 import javax.swing.AbstractAction;
116 import javax.swing.ActionMap;
117 import javax.swing.ImageIcon;
118 import javax.swing.InputMap;
119 import javax.swing.JButton;
120 import javax.swing.JComponent;
121 import javax.swing.JPanel;
122 import javax.swing.JPopupMenu;
123 import javax.swing.KeyStroke;
124 import javax.swing.RepaintManager;
125 import javax.swing.SwingUtilities;
126 import javax.swing.Timer;
127 import javax.swing.border.Border;
128 import javax.swing.event.AncestorEvent;
129 import javax.swing.event.AncestorListener;
130 import javax.swing.event.ChangeEvent;
131 import javax.swing.event.ChangeListener;
132 import javax.swing.event.MouseInputAdapter;
133 import javax.vecmath.Color3f;
134 import javax.vecmath.Point3d;
135 import javax.vecmath.Point3f;
136 import javax.vecmath.TexCoord2f;
137 import javax.vecmath.Vector3f;
138 import javax.vecmath.Vector4f;
139 
140 import com.eteks.sweethome3d.j3d.Component3DManager;
141 import com.eteks.sweethome3d.j3d.Ground3D;
142 import com.eteks.sweethome3d.j3d.HomePieceOfFurniture3D;
143 import com.eteks.sweethome3d.j3d.ModelManager;
144 import com.eteks.sweethome3d.j3d.Object3DBranch;
145 import com.eteks.sweethome3d.j3d.Object3DBranchFactory;
146 import com.eteks.sweethome3d.j3d.TextureManager;
147 import com.eteks.sweethome3d.j3d.Wall3D;
148 import com.eteks.sweethome3d.model.Camera;
149 import com.eteks.sweethome3d.model.CollectionEvent;
150 import com.eteks.sweethome3d.model.CollectionListener;
151 import com.eteks.sweethome3d.model.Elevatable;
152 import com.eteks.sweethome3d.model.Home;
153 import com.eteks.sweethome3d.model.HomeDoorOrWindow;
154 import com.eteks.sweethome3d.model.HomeEnvironment;
155 import com.eteks.sweethome3d.model.HomeFurnitureGroup;
156 import com.eteks.sweethome3d.model.HomeLight;
157 import com.eteks.sweethome3d.model.HomePieceOfFurniture;
158 import com.eteks.sweethome3d.model.HomeTexture;
159 import com.eteks.sweethome3d.model.Label;
160 import com.eteks.sweethome3d.model.Level;
161 import com.eteks.sweethome3d.model.Polyline;
162 import com.eteks.sweethome3d.model.Room;
163 import com.eteks.sweethome3d.model.Selectable;
164 import com.eteks.sweethome3d.model.UserPreferences;
165 import com.eteks.sweethome3d.model.Wall;
166 import com.eteks.sweethome3d.tools.OperatingSystem;
167 import com.eteks.sweethome3d.viewcontroller.HomeController3D;
168 import com.eteks.sweethome3d.viewcontroller.Object3DFactory;
169 import com.sun.j3d.exp.swing.JCanvas3D;
170 import com.sun.j3d.utils.geometry.GeometryInfo;
171 import com.sun.j3d.utils.picking.PickCanvas;
172 import com.sun.j3d.utils.picking.PickResult;
173 import com.sun.j3d.utils.universe.SimpleUniverse;
174 import com.sun.j3d.utils.universe.Viewer;
175 import com.sun.j3d.utils.universe.ViewingPlatform;
176 
177 /**
178  * A component that displays home walls, rooms and furniture with Java 3D.
179  * @author Emmanuel Puybaret
180  */
181 public class HomeComponent3D extends JComponent implements com.eteks.sweethome3d.viewcontroller.View, Printable {
182   private enum ActionType {MOVE_CAMERA_FORWARD, MOVE_CAMERA_FAST_FORWARD, MOVE_CAMERA_BACKWARD, MOVE_CAMERA_FAST_BACKWARD,
183       MOVE_CAMERA_LEFT, MOVE_CAMERA_FAST_LEFT, MOVE_CAMERA_RIGHT, MOVE_CAMERA_FAST_RIGHT,
184       ROTATE_CAMERA_YAW_LEFT, ROTATE_CAMERA_YAW_FAST_LEFT, ROTATE_CAMERA_YAW_RIGHT, ROTATE_CAMERA_YAW_FAST_RIGHT,
185       ROTATE_CAMERA_PITCH_UP, ROTATE_CAMERA_PITCH_FAST_UP, ROTATE_CAMERA_PITCH_DOWN, ROTATE_CAMERA_PITCH_FAST_DOWN,
186       ELEVATE_CAMERA_UP, ELEVATE_CAMERA_FAST_UP, ELEVATE_CAMERA_DOWN, ELEVATE_CAMERA_FAST_DOWN}
187 
188   private static final boolean JAVA3D_1_5 = VirtualUniverse.getProperties().get("j3d.version") != null
189       && ((String)VirtualUniverse.getProperties().get("j3d.version")).startsWith("1.5");
190 
191   private final Home                               home;
192   private final boolean                            displayShadowOnFloor;
193   private final Object3DFactory                    object3dFactory;
194   private final Map<Selectable, Object3DBranch>    homeObjects = new HashMap<Selectable, Object3DBranch>();
195   private Light []                                 sceneLights;
196   private Collection<Selectable>                   homeObjectsToUpdate;
197   private Collection<Selectable>                   lightScopeObjectsToUpdate;
198   private Component                                component3D;
199   private SimpleUniverse                           onscreenUniverse;
200   private Camera                                   camera;
201   // Listeners bound to home that updates 3D scene objects
202   private PropertyChangeListener                   cameraChangeListener;
203   private PropertyChangeListener                   homeCameraListener;
204   private PropertyChangeListener                   backgroundChangeListener;
205   private PropertyChangeListener                   groundChangeListener;
206   private PropertyChangeListener                   backgroundLightColorListener;
207   private PropertyChangeListener                   lightColorListener;
208   private PropertyChangeListener                   subpartSizeListener;
209   private PropertyChangeListener                   elevationChangeListener;
210   private PropertyChangeListener                   wallsAlphaListener;
211   private PropertyChangeListener                   drawingModeListener;
212   private CollectionListener<Level>                levelListener;
213   private PropertyChangeListener                   levelChangeListener;
214   private CollectionListener<Wall>                 wallListener;
215   private PropertyChangeListener                   wallChangeListener;
216   private CollectionListener<HomePieceOfFurniture> furnitureListener;
217   private PropertyChangeListener                   furnitureChangeListener;
218   private CollectionListener<Room>                 roomListener;
219   private PropertyChangeListener                   roomChangeListener;
220   private CollectionListener<Polyline>             polylineListener;
221   private PropertyChangeListener                   polylineChangeListener;
222   private CollectionListener<Label>                labelListener;
223   private PropertyChangeListener                   labelChangeListener;
224   // Offscreen printed image cache
225   // Creating an offscreen buffer is a quite lengthy operation so we keep the last printed image in this field
226   // This image should be set to null each time the 3D view changes
227   private BufferedImage                            printedImageCache;
228   private BoundingBox                              approximateHomeBoundsCache;
229   private SimpleUniverse                           offscreenUniverse;
230 
231   private JComponent                               navigationPanel;
232   private ComponentListener                        navigationPanelListener;
233   private BufferedImage                            navigationPanelImage;
234   private Area                                     lightScopeOutsideWallsAreaCache;
235 
236   /**
237    * Creates a 3D component that displays <code>home</code> walls, rooms and furniture,
238    * with no controller.
239    * @throws IllegalStateException  if the 3D component couldn't be created.
240    */
HomeComponent3D(Home home)241   public HomeComponent3D(Home home) {
242     this(home, null);
243   }
244 
245   /**
246    * Creates a 3D component that displays <code>home</code> walls, rooms and furniture.
247    * @throws IllegalStateException  if the 3D component couldn't be created.
248    */
HomeComponent3D(Home home, HomeController3D controller)249   public HomeComponent3D(Home home, HomeController3D controller) {
250     this(home, null, controller);
251   }
252 
253   /**
254    * Creates a 3D component that displays <code>home</code> walls, rooms and furniture,
255    * with shadows on the floor.
256    * @throws IllegalStateException  if the 3D component couldn't be created.
257    */
HomeComponent3D(Home home, UserPreferences preferences, boolean displayShadowOnFloor)258   public HomeComponent3D(Home home,
259                          UserPreferences  preferences,
260                          boolean displayShadowOnFloor) {
261     this(home, preferences, new Object3DBranchFactory(preferences), displayShadowOnFloor, null);
262   }
263 
264   /**
265    * Creates a 3D component that displays <code>home</code> walls, rooms and furniture.
266    * @throws IllegalStateException  if the 3D component couldn't be created.
267    */
HomeComponent3D(Home home, UserPreferences preferences, HomeController3D controller)268   public HomeComponent3D(Home home,
269                          UserPreferences  preferences,
270                          HomeController3D controller) {
271     this(home, preferences, new Object3DBranchFactory(preferences), false, controller);
272   }
273 
274   /**
275    * Creates a 3D component that displays <code>home</code> walls, rooms and furniture.
276    * @param home the home to display in this component
277    * @param preferences user preferences
278    * @param object3dFactory a factory able to create 3D objects from <code>home</code> items.
279    *            The {@link Object3DFactory#createObject3D(Home, Selectable, boolean) createObject3D} of
280    *            this factory is expected to return an instance of {@link Object3DBranch} in current implementation.
281    * @param controller the controller that manages modifications in <code>home</code>.
282    * @throws IllegalStateException  if the 3D component couldn't be created.
283    */
HomeComponent3D(Home home, UserPreferences preferences, Object3DFactory object3dFactory, HomeController3D controller)284   public HomeComponent3D(Home home,
285                          UserPreferences  preferences,
286                          Object3DFactory  object3dFactory,
287                          HomeController3D controller) {
288     this(home, preferences, object3dFactory, false, controller);
289   }
290 
291   /**
292    * Creates a 3D component that displays <code>home</code> walls, rooms and furniture.
293    * @throws IllegalStateException  if the 3D component couldn't be created.
294    */
HomeComponent3D(Home home, UserPreferences preferences, Object3DFactory object3dFactory, boolean displayShadowOnFloor, HomeController3D controller)295   public HomeComponent3D(Home home,
296                          UserPreferences  preferences,
297                          Object3DFactory  object3dFactory,
298                          boolean displayShadowOnFloor,
299                          HomeController3D controller) {
300     this.home = home;
301     this.displayShadowOnFloor = displayShadowOnFloor;
302     this.object3dFactory = object3dFactory != null
303         ? object3dFactory
304         : new Object3DBranchFactory(preferences);
305 
306     if (controller != null) {
307       createActions(controller);
308       installKeyboardActions();
309       // Let this component manage focus
310       setFocusable(true);
311       SwingTools.installFocusBorder(this);
312     }
313 
314     GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
315     if (graphicsEnvironment.getScreenDevices().length == 1) {
316       // If only one screen device is available, create canvas 3D immediately,
317       // otherwise create it once the screen device of the parent is known
318       createComponent3D(graphicsEnvironment.getDefaultScreenDevice().getDefaultConfiguration(), preferences, controller);
319     }
320 
321     // Add an ancestor listener to create canvas 3D and its universe once this component is made visible
322     // and clean up universe once its parent frame is disposed
323     addAncestorListener(preferences, controller, displayShadowOnFloor);
324   }
325 
326   /**
327    * Adds an ancestor listener to this component to manage the creation of the canvas and its universe
328    * and clean up the universe.
329    */
addAncestorListener(final UserPreferences preferences, final HomeController3D controller, final boolean displayShadowOnFloor)330   private void addAncestorListener(final UserPreferences preferences,
331                                    final HomeController3D controller,
332                                    final boolean displayShadowOnFloor) {
333     addAncestorListener(new AncestorListener() {
334         public void ancestorAdded(AncestorEvent ev) {
335           if (offscreenUniverse != null) {
336             throw new IllegalStateException("Can't listen to home changes offscreen and onscreen at the same time");
337           }
338 
339           // Create component 3D only once it's visible
340           Insets insets = getInsets();
341           if (getHeight() <= insets.top + insets.bottom
342               || getWidth() <= insets.left + insets.right) {
343             addComponentListener(new ComponentAdapter() {
344                 @Override
345                 public void componentResized(ComponentEvent ev) {
346                   removeComponentListener(this);
347                   // If 3D view is still in component hierarchy, create component children
348                   if (SwingUtilities.getRoot(HomeComponent3D.this) != null) {
349                     ancestorAdded(null);
350                   }
351                 }
352               });
353             return;
354           } else if (ev == null) {
355             // Force a resize event to make the component 3D appear
356             Component root = SwingUtilities.getRoot(HomeComponent3D.this);
357             root.dispatchEvent(new ComponentEvent(root, ComponentEvent.COMPONENT_RESIZED));
358           }
359 
360           // Create component 3D only once the graphics configuration of its parent is known
361           if (component3D == null) {
362             createComponent3D(getGraphicsConfiguration(), preferences, controller);
363           }
364           if (onscreenUniverse == null) {
365             onscreenUniverse = createUniverse(displayShadowOnFloor, true, false);
366             Canvas3D canvas3D;
367             if (component3D instanceof Canvas3D) {
368               canvas3D = (Canvas3D)component3D;
369             } else {
370               try {
371                 // Call JCanvas3D#getOffscreenCanvas3D by reflection to be able to run under Java 3D 1.3
372                 canvas3D = (Canvas3D)Class.forName("com.sun.j3d.exp.swing.JCanvas3D").getMethod("getOffscreenCanvas3D").invoke(component3D);
373               } catch (Exception ex) {
374                 UnsupportedOperationException ex2 = new UnsupportedOperationException();
375                 ex2.initCause(ex);
376                 throw ex2;
377               }
378             }
379             // Bind universe to canvas3D
380             onscreenUniverse.getViewer().getView().addCanvas3D(canvas3D);
381             component3D.setFocusable(false);
382             updateNavigationPanelImage();
383           }
384         }
385 
386         public void ancestorRemoved(AncestorEvent ev) {
387           if (onscreenUniverse != null) {
388             onscreenUniverse.cleanup();
389             removeHomeListeners();
390             onscreenUniverse = null;
391           }
392           if (component3D != null) {
393             removeAll();
394             for (MouseListener l : component3D.getMouseListeners()) {
395               component3D.removeMouseListener(l);
396             }
397             for (MouseMotionListener l : component3D.getMouseMotionListeners()) {
398               component3D.removeMouseMotionListener(l);
399             }
400             for (MouseWheelListener l : component3D.getMouseWheelListeners()) {
401               component3D.removeMouseWheelListener(l);
402             }
403             component3D = null;
404             navigationPanel = null;
405           }
406         }
407 
408         public void ancestorMoved(AncestorEvent ev) {
409         }
410       });
411   }
412 
413   /**
414    * Creates the 3D component associated with the given <code>configuration</code> device.
415    */
createComponent3D(GraphicsConfiguration configuration, UserPreferences preferences, HomeController3D controller)416   private void createComponent3D(GraphicsConfiguration configuration,
417                                  UserPreferences  preferences,
418                                  HomeController3D controller) {
419     if (Boolean.getBoolean("com.eteks.sweethome3d.j3d.useOffScreen3DView")) {
420       GraphicsConfigTemplate3D template = new GraphicsConfigTemplate3D();
421       template.setSceneAntialiasing(GraphicsConfigTemplate3D.PREFERRED);
422       // Request depth size equal to 24 if supported
423       int defaultDepthSize = template.getDepthSize();
424       template.setDepthSize(24);
425       if (!template.isGraphicsConfigSupported(
426           GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration())) {
427         template.setDepthSize(defaultDepthSize);
428       }
429       try {
430         // Instantiate JCanvas3DWithNavigationPanel inner class by reflection
431         // to be able to run under Java 3D 1.3
432         this.component3D = (Component)Class.forName(getClass().getName() + "$JCanvas3DWithNavigationPanel").
433             getConstructor(getClass(), GraphicsConfigTemplate3D.class).newInstance(this, template);
434         this.component3D.setSize(1, 1);
435       } catch (ClassNotFoundException ex) {
436         throw new UnsupportedOperationException("Java 3D 1.5 required to display an offscreen 3D view");
437       } catch (Exception ex) {
438         UnsupportedOperationException ex2 = new UnsupportedOperationException();
439         ex2.initCause(ex);
440         throw ex2;
441       }
442     } else {
443       this.component3D = Component3DManager.getInstance().getOnscreenCanvas3D(configuration,
444           new Component3DManager.RenderingObserver() {
445               private Shape3D dummyShape;
446 
447               public void canvas3DSwapped(Canvas3D canvas3D) {
448               }
449 
450               public void canvas3DPreRendered(Canvas3D canvas3D) {
451               }
452 
453               public void canvas3DPostRendered(Canvas3D canvas3D) {
454                 // Copy reference to navigation panel image to avoid concurrency problems
455                 // if it's modified in the EDT while this method draws it
456                 BufferedImage navigationPanelImage = HomeComponent3D.this.navigationPanelImage;
457                 // Render navigation panel upon canvas 3D if it exists
458                 if (navigationPanelImage != null) {
459                   if (JAVA3D_1_5) {
460                     // Render trivial transparent shape to reset the possible transformation set on the last rendered texture
461                     // See https://jogamp.org/bugzilla/show_bug.cgi?id=1006#c1
462                     if (this.dummyShape == null) {
463                       PointArray dummyGeometry = new PointArray(1, PointArray.COORDINATES);
464                       dummyGeometry.setCoordinates(0, new float [] {0, 0, 0});
465                       Appearance appearance = new Appearance();
466                       appearance.setTransparencyAttributes(new TransparencyAttributes(TransparencyAttributes.FASTEST, 1));
467                       this.dummyShape = new Shape3D(dummyGeometry, appearance);
468                     }
469                     canvas3D.getGraphicsContext3D().draw(this.dummyShape);
470                   }
471                   J3DGraphics2D g2D = canvas3D.getGraphics2D();
472                   g2D.drawImage(navigationPanelImage, null, 0, 0);
473                   g2D.flush(true);
474                 }
475               }
476             });
477     }
478     this.component3D.setBackground(new Color(230, 230, 230));
479 
480     JPanel canvasPanel = new JPanel(new LayoutManager() {
481         public void addLayoutComponent(String name, Component comp) {
482         }
483 
484         public void removeLayoutComponent(Component comp) {
485         }
486 
487         public Dimension preferredLayoutSize(Container parent) {
488           return component3D.getPreferredSize();
489         }
490 
491         public Dimension minimumLayoutSize(Container parent) {
492           return component3D.getMinimumSize();
493         }
494 
495         public void layoutContainer(Container parent) {
496           component3D.setBounds(0, 0, Math.max(1, parent.getWidth()), Math.max(1, parent.getHeight()));
497           if (navigationPanel != null
498               && navigationPanel.isVisible()) {
499             // Ensure that navigationPanel is always in top corner
500             Dimension preferredSize = navigationPanel.getPreferredSize();
501             navigationPanel.setBounds(0, 0, preferredSize.width, preferredSize.height);
502           }
503         }
504       });
505 
506     canvasPanel.add(this.component3D);
507     setLayout(new GridLayout());
508     add(canvasPanel);
509     if (controller != null) {
510       addMouseListeners(controller, this.component3D);
511       // Add mouse listeners again to ensure 3D component will receive events
512       for (MouseListener l : getMouseListeners()) {
513         super.removeMouseListener(l);
514         addMouseListener(l);
515       }
516       for (MouseMotionListener l : getMouseMotionListeners()) {
517         super.removeMouseMotionListener(l);
518         addMouseMotionListener(l);
519       }
520       if (preferences != null
521           && (!OperatingSystem.isMacOSX()
522               || OperatingSystem.isMacOSXLeopardOrSuperior())) {
523         // No support for navigation panel under Mac OS X Tiger
524         // (too unstable, may crash system at 3D view resizing)
525         this.navigationPanel = createNavigationPanel(this.home, preferences, controller);
526         setNavigationPanelVisible(preferences.isNavigationPanelVisible() && isVisible());
527         preferences.addPropertyChangeListener(UserPreferences.Property.NAVIGATION_PANEL_VISIBLE,
528             new NavigationPanelChangeListener(this));
529       }
530       createActions(controller);
531       installKeyboardActions();
532       // Let this component manage focus
533       setFocusable(true);
534       SwingTools.installFocusBorder(this);
535     }
536   }
537 
538   /**
539    * A <code>JCanvas</code> canvas that displays the navigation panel of a home component 3D upon it.
540    */
541   private static class JCanvas3DWithNavigationPanel extends JCanvas3D {
542     private final HomeComponent3D homeComponent3D;
543 
JCanvas3DWithNavigationPanel(HomeComponent3D homeComponent3D, GraphicsConfigTemplate3D template)544     public JCanvas3DWithNavigationPanel(HomeComponent3D homeComponent3D,
545                                         GraphicsConfigTemplate3D template) {
546       super(template);
547       this.homeComponent3D = homeComponent3D;
548     }
549 
paintComponent(Graphics g)550     public void paintComponent(Graphics g) {
551       super.paintComponent(g);
552       g.drawImage(this.homeComponent3D.navigationPanelImage, 0, 0, this);
553     }
554   }
555 
556   @Override
setVisible(boolean visible)557   public void setVisible(boolean visible) {
558     super.setVisible(visible);
559     if (this.component3D != null) {
560       this.component3D.setVisible(visible);
561     }
562   }
563 
564   /**
565    * Preferences property listener bound to this component with a weak reference to avoid
566    * strong link between preferences and this component.
567    */
568   private static class NavigationPanelChangeListener implements PropertyChangeListener {
569     private final WeakReference<HomeComponent3D>  homeComponent3D;
570 
NavigationPanelChangeListener(HomeComponent3D homeComponent3D)571     public NavigationPanelChangeListener(HomeComponent3D homeComponent3D) {
572       this.homeComponent3D = new WeakReference<HomeComponent3D>(homeComponent3D);
573     }
574 
propertyChange(PropertyChangeEvent ev)575     public void propertyChange(PropertyChangeEvent ev) {
576       // If home pane was garbage collected, remove this listener from preferences
577       HomeComponent3D homeComponent3D = this.homeComponent3D.get();
578       if (homeComponent3D == null) {
579         ((UserPreferences)ev.getSource()).removePropertyChangeListener(
580             UserPreferences.Property.NAVIGATION_PANEL_VISIBLE, this);
581       } else {
582         homeComponent3D.setNavigationPanelVisible((Boolean)ev.getNewValue() && homeComponent3D.isVisible());
583       }
584     }
585   }
586 
587   /**
588    * Returns the component displayed as navigation panel by this 3D view.
589    */
createNavigationPanel(Home home, UserPreferences preferences, HomeController3D controller)590   private JComponent createNavigationPanel(Home home,
591                                            UserPreferences preferences,
592                                            HomeController3D controller) {
593     JPanel navigationPanel = new JPanel(new GridBagLayout()) {
594         @Override
595         public void applyComponentOrientation(ComponentOrientation o) {
596           // Ignore orientation
597         }
598       };
599     String navigationPanelIconPath = preferences.getLocalizedString(HomeComponent3D.class, "navigationPanel.icon");
600     final ImageIcon nagivationPanelIcon = navigationPanelIconPath.length() > 0
601         ? new ImageIcon(HomeComponent3D.class.getResource(navigationPanelIconPath))
602         : null;
603     navigationPanel.setBorder(new Border() {
604         public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
605           if (nagivationPanelIcon != null) {
606             nagivationPanelIcon.paintIcon(c, g, x, y);
607           } else {
608             // Draw a surrounding oval if no navigation panel icon is defined
609             Graphics2D g2D = (Graphics2D)g;
610             g2D.setColor(Color.BLACK);
611             g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
612             g2D.drawOval(x + 3, y + 3, width - 6, height - 6);
613           }
614         }
615 
616         public Insets getBorderInsets(Component c) {
617           return new Insets(2, 2, 2, 2);
618         }
619 
620         public boolean isBorderOpaque() {
621           return false;
622         }
623       });
624     navigationPanel.setOpaque(false);
625     navigationPanel.add(new NavigationButton(0, -(float)Math.PI / 36, 0, "TURN_LEFT", preferences, controller),
626         new GridBagConstraints(0, 1, 1, 2, 0, 0,
627             GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 3, 0, 0), 0, 0));
628     navigationPanel.add(new NavigationButton(12.5f, 0, 0, "GO_FORWARD", preferences, controller),
629         new GridBagConstraints(1, 0, 1, 1, 0, 0,
630             GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(3, 0, 0, 0), 0, 0));
631     navigationPanel.add(new NavigationButton(0, (float)Math.PI / 36, 0, "TURN_RIGHT", preferences, controller),
632         new GridBagConstraints(2, 1, 1, 2, 0, 0,
633             GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 2), 0, 0));
634     navigationPanel.add(new NavigationButton(-12.5f, 0, 0, "GO_BACKWARD", preferences, controller),
635         new GridBagConstraints(1, 3, 1, 1, 0, 0,
636             GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 2, 0), 0, 0));
637     navigationPanel.add(new NavigationButton(0, 0, -(float)Math.PI / 100, "TURN_UP", preferences, controller),
638         new GridBagConstraints(1, 1, 1, 1, 0, 0,
639             GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(1, 1, 1, 1), 0, 0));
640     navigationPanel.add(new NavigationButton(0, 0, (float)Math.PI / 100, "TURN_DOWN", preferences, controller),
641         new GridBagConstraints(1, 2, 1, 1, 0, 0,
642             GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 1, 0), 0, 0));
643     return navigationPanel;
644   }
645 
646   /**
647    * An icon button that changes camera location and angles when pressed.
648    */
649   private static class NavigationButton extends JButton {
650     private boolean shiftDown;
651 
NavigationButton(final float moveDelta, final float yawDelta, final float pitchDelta, String actionName, UserPreferences preferences, final HomeController3D controller)652     public NavigationButton(final float moveDelta,
653                             final float yawDelta,
654                             final float pitchDelta,
655                             String actionName,
656                             UserPreferences preferences,
657                             final HomeController3D controller) {
658       super(new ResourceAction(preferences, HomeComponent3D.class, actionName, true) {
659           @Override
660           public void actionPerformed(ActionEvent ev) {
661             // Manage auto repeat button with mouse listener
662           }
663         });
664       // Create a darker press icon
665       setPressedIcon(new ImageIcon(createImage(new FilteredImageSource(
666           ((ImageIcon)getIcon()).getImage().getSource(),
667           new RGBImageFilter() {
668             {
669               canFilterIndexColorModel = true;
670             }
671 
672             public int filterRGB (int x, int y, int rgb) {
673               // Return darker color
674               int alpha = rgb & 0xFF000000;
675               int darkerRed = ((rgb & 0xFF0000) >> 1) & 0xFF0000;
676               int darkerGreen  = ((rgb & 0x00FF00) >> 1) & 0x00FF00;
677               int darkerBlue  = (rgb & 0x0000FF) >> 1;
678               return alpha | darkerRed | darkerGreen | darkerBlue;
679             }
680           }))));
681 
682       // Track shift key press
683       addMouseMotionListener(new MouseMotionAdapter() {
684           @Override
685           public void mouseDragged(MouseEvent ev) {
686             shiftDown = ev.isShiftDown();
687           }
688         });
689       addMouseListener(new MouseAdapter() {
690           @Override
691           public void mousePressed(MouseEvent ev) {
692             shiftDown = ev.isShiftDown();
693             SwingUtilities.getAncestorOfClass(HomeComponent3D.class,
694                 NavigationButton.this).requestFocusInWindow();
695           }
696         });
697 
698       // Create a timer that will update camera angles and location
699       final Timer timer = new Timer(50, new ActionListener() {
700           public void actionPerformed(ActionEvent ev) {
701             controller.moveCamera(shiftDown ? moveDelta : moveDelta / 5);
702             controller.rotateCameraYaw(shiftDown ? yawDelta : yawDelta / 5);
703             controller.rotateCameraPitch(pitchDelta);
704           }
705         });
706       timer.setInitialDelay(0);
707 
708       // Update camera when button is armed
709       addChangeListener(new ChangeListener() {
710           public void stateChanged(ChangeEvent ev) {
711             if (getModel().isArmed()
712                 && !timer.isRunning()) {
713               timer.restart();
714             } else if (!getModel().isArmed()
715                        && timer.isRunning()) {
716               timer.stop();
717             }
718           }
719         });
720       setFocusable(false);
721       setBorder(null);
722       setContentAreaFilled(false);
723       // Force preferred size to ensure button isn't larger
724       setPreferredSize(new Dimension(getIcon().getIconWidth(), getIcon().getIconHeight()));
725       addPropertyChangeListener(JButton.ICON_CHANGED_PROPERTY, new PropertyChangeListener() {
726           public void propertyChange(PropertyChangeEvent ev) {
727             // Reset border when icon is reset after a resource action change
728             setBorder(null);
729           }
730         });
731     }
732   }
733 
734   /**
735    * Sets the component that will be drawn upon the heavyweight 3D component shown by this component.
736    * Mouse events will targeted to the navigation panel when needed.
737    * Supports transparent components.
738    */
setNavigationPanelVisible(boolean visible)739   private void setNavigationPanelVisible(boolean visible) {
740     if (this.navigationPanel != null) {
741       this.navigationPanel.setVisible(visible);
742       if (visible) {
743         // Add a component listener that updates navigation panel image
744         this.navigationPanelListener = new ComponentAdapter() {
745             @Override
746             public void componentResized(ComponentEvent ev) {
747               updateNavigationPanelImage();
748             }
749 
750             @Override
751             public void componentMoved(ComponentEvent e) {
752               updateNavigationPanelImage();
753             }
754           };
755         this.navigationPanel.addComponentListener(this.navigationPanelListener);
756         // Add the navigation panel to this component to be able to paint it
757         // but show it behind canvas 3D
758         this.component3D.getParent().add(this.navigationPanel);
759       } else {
760         this.navigationPanel.removeComponentListener(this.navigationPanelListener);
761         if (this.navigationPanel.getParent() != null) {
762           this.navigationPanel.getParent().remove(this.navigationPanel);
763         }
764       }
765       revalidate();
766       updateNavigationPanelImage();
767       this.component3D.repaint();
768     }
769   }
770 
771   /**
772    * Updates the image of the components that may overlap canvas 3D
773    * (with a Z order smaller than the one of the canvas 3D).
774    */
updateNavigationPanelImage()775   private void updateNavigationPanelImage() {
776     if (this.navigationPanel != null
777         && this.navigationPanel.isVisible()) {
778       Rectangle componentBounds = this.navigationPanel.getBounds();
779       Rectangle imageSize = new Rectangle(this.component3D.getX(), this.component3D.getY());
780       imageSize.add(componentBounds.x + componentBounds.width,
781           componentBounds.y + componentBounds.height);
782       if (!imageSize.isEmpty()) {
783         BufferedImage updatedImage = this.navigationPanelImage;
784         // Consider that no navigation panel image is available
785         // while it's updated
786         this.navigationPanelImage = null;
787         Graphics2D g2D;
788         if (updatedImage == null
789             || updatedImage.getWidth() != imageSize.width
790             || updatedImage.getHeight() != imageSize.height) {
791           updatedImage = new BufferedImage(
792               imageSize.width, imageSize.height, BufferedImage.TYPE_INT_ARGB);
793           g2D = (Graphics2D)updatedImage.getGraphics();
794         } else {
795           // Clear image
796           g2D = (Graphics2D)updatedImage.getGraphics();
797           Composite oldComposite = g2D.getComposite();
798           g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0));
799           g2D.fill(new Rectangle2D.Double(0, 0, imageSize.width, imageSize.height));
800           g2D.setComposite(oldComposite);
801         }
802         this.navigationPanel.paintAll(g2D);
803         g2D.dispose();
804         // Navigation panel image ready to be displayed
805         this.navigationPanelImage = updatedImage;
806         return;
807       }
808     }
809     this.navigationPanelImage = null;
810   }
811 
812   /**
813    * Returns a new 3D universe that displays <code>home</code> objects.
814    */
createUniverse(boolean displayShadowOnFloor, boolean listenToHomeUpdates, boolean waitForLoading)815   private SimpleUniverse createUniverse(boolean displayShadowOnFloor,
816                                         boolean listenToHomeUpdates,
817                                         boolean waitForLoading) {
818     // Create a universe bound to no canvas 3D
819     ViewingPlatform viewingPlatform = new ViewingPlatform();
820     // Add an interpolator to view transform to get smooth transition
821     TransformGroup viewPlatformTransform = viewingPlatform.getViewPlatformTransform();
822     CameraInterpolator cameraInterpolator = new CameraInterpolator(viewPlatformTransform);
823     cameraInterpolator.setSchedulingBounds(new BoundingSphere(new Point3d(), 1E7));
824     viewPlatformTransform.addChild(cameraInterpolator);
825     viewPlatformTransform.setCapability(TransformGroup.ALLOW_CHILDREN_READ);
826 
827     Viewer viewer = new Viewer(new Canvas3D [0]);
828     SimpleUniverse universe = new SimpleUniverse(viewingPlatform, viewer);
829 
830     View view = viewer.getView();
831     view.setTransparencySortingPolicy(View.TRANSPARENCY_SORT_GEOMETRY);
832 
833     // Update field of view from current camera
834     updateView(view, this.home.getCamera());
835 
836     // Update point of view from current camera
837     updateViewPlatformTransform(viewPlatformTransform, this.home.getCamera(), false);
838 
839     // Add camera listeners to update later point of view from camera
840     if (listenToHomeUpdates) {
841       addCameraListeners(view, viewPlatformTransform);
842     }
843 
844     // Link scene matching home to universe
845     universe.addBranchGraph(createSceneTree(
846         displayShadowOnFloor, listenToHomeUpdates, waitForLoading));
847 
848     return universe;
849   }
850 
851   /**
852    * Remove all listeners bound to home that updates 3D scene objects.
853    */
removeHomeListeners()854   private void removeHomeListeners() {
855     this.home.removePropertyChangeListener(Home.Property.CAMERA, this.homeCameraListener);
856     HomeEnvironment homeEnvironment = this.home.getEnvironment();
857     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.SKY_COLOR, this.backgroundChangeListener);
858     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.SKY_TEXTURE, this.backgroundChangeListener);
859     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.GROUND_COLOR, this.backgroundChangeListener);
860     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.GROUND_TEXTURE, this.backgroundChangeListener);
861     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.GROUND_COLOR, this.groundChangeListener);
862     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.GROUND_TEXTURE, this.groundChangeListener);
863     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.BACKGROUND_IMAGE_VISIBLE_ON_GROUND_3D, this.groundChangeListener);
864     this.home.removePropertyChangeListener(Home.Property.BACKGROUND_IMAGE, this.groundChangeListener);
865     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.LIGHT_COLOR, this.backgroundLightColorListener);
866     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.LIGHT_COLOR, this.lightColorListener);
867     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.WALLS_ALPHA, this.wallsAlphaListener);
868     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.DRAWING_MODE, this.drawingModeListener);
869     homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.SUBPART_SIZE_UNDER_LIGHT, this.subpartSizeListener);
870     this.home.getCamera().removePropertyChangeListener(this.cameraChangeListener);
871     this.home.removePropertyChangeListener(Home.Property.CAMERA, this.elevationChangeListener);
872     this.home.getCamera().removePropertyChangeListener(this.elevationChangeListener);
873     this.home.removeLevelsListener(this.levelListener);
874     for (Level level : this.home.getLevels()) {
875       level.removePropertyChangeListener(this.levelChangeListener);
876     }
877     this.home.removeWallsListener(this.wallListener);
878     for (Wall wall : this.home.getWalls()) {
879       wall.removePropertyChangeListener(this.wallChangeListener);
880     }
881     this.home.removeFurnitureListener(this.furnitureListener);
882     for (HomePieceOfFurniture piece : this.home.getFurniture()) {
883       removePropertyChangeListener(piece, this.furnitureChangeListener);
884     }
885     this.home.removeRoomsListener(this.roomListener);
886     for (Room room : this.home.getRooms()) {
887       room.removePropertyChangeListener(this.roomChangeListener);
888     }
889     this.home.removePolylinesListener(this.polylineListener);
890     for (Polyline polyline : this.home.getPolylines()) {
891       polyline.removePropertyChangeListener(this.polylineChangeListener);
892     }
893     this.home.removeLabelsListener(this.labelListener);
894     for (Label label : this.home.getLabels()) {
895       label.removePropertyChangeListener(this.labelChangeListener);
896     }
897   }
898 
899   /**
900    * Prints this component to make it fill <code>pageFormat</code> imageable size.
901    */
print(Graphics g, PageFormat pageFormat, int pageIndex)902   public int print(Graphics g, PageFormat pageFormat, int pageIndex) {
903     if (pageIndex == 0) {
904       // Compute printed image size to render 3D view in 150 dpi
905       double printSize = Math.min(pageFormat.getImageableWidth(),
906           pageFormat.getImageableHeight());
907       int printedImageSize = (int)(printSize / 72 * 150);
908       if (this.printedImageCache == null
909           || this.printedImageCache.getWidth() != printedImageSize) {
910         try {
911           this.printedImageCache = getOffScreenImage(printedImageSize, printedImageSize);
912         } catch (IllegalRenderingStateException ex) {
913           // If off screen canvas failed, consider that 3D view page doesn't exist
914           return NO_SUCH_PAGE;
915         }
916       }
917 
918       Graphics2D g2D = (Graphics2D)g.create();
919       // Center the 3D view in component
920       g2D.translate(pageFormat.getImageableX() + (pageFormat.getImageableWidth() - printSize) / 2,
921           pageFormat.getImageableY() + (pageFormat.getImageableHeight() - printSize) / 2);
922       double scale = printSize / printedImageSize;
923       g2D.scale(scale, scale);
924       g2D.drawImage(this.printedImageCache, 0, 0, this);
925       g2D.dispose();
926 
927       return PAGE_EXISTS;
928     } else {
929       return NO_SUCH_PAGE;
930     }
931   }
932 
933   /**
934    * Optimizes this component for the creation of a sequence of multiple off screen images.
935    * Once off screen images are generated with {@link #getOffScreenImage(int, int) getOffScreenImage},
936    * call {@link #endOffscreenImagesCreation() endOffscreenImagesCreation} method to free resources.
937    */
startOffscreenImagesCreation()938   public void startOffscreenImagesCreation() {
939     if (this.offscreenUniverse == null) {
940       if (this.onscreenUniverse != null) {
941         throw new IllegalStateException("Can't listen to home changes offscreen and onscreen at the same time");
942       }
943       this.offscreenUniverse = createUniverse(this.displayShadowOnFloor, true, true);
944       // Replace textures by clones because Java 3D doesn't accept all the time
945       // to share textures between offscreen and onscreen environments
946       Map<Texture, Texture> replacedTextures = new HashMap<Texture, Texture>();
947       for (Enumeration it = this.offscreenUniverse.getLocale().getAllBranchGraphs(); it.hasMoreElements(); ) {
948         cloneTexture((Node)it.nextElement(), replacedTextures);
949       }
950     }
951   }
952 
953   /**
954    * Returns an image of the home viewed by this component at the given size.
955    */
getOffScreenImage(int width, int height)956   public BufferedImage getOffScreenImage(int width, int height) {
957     List<Selectable> selectedItems = this.home.getSelectedItems();
958     SimpleUniverse offScreenImageUniverse = null;
959     try {
960       View view;
961       if (this.offscreenUniverse == null) {
962         offScreenImageUniverse = createUniverse(this.displayShadowOnFloor, false, true);
963         view = offScreenImageUniverse.getViewer().getView();
964         // Replace textures by clones because Java 3D doesn't accept all the time
965         // to share textures between offscreen and onscreen environments
966         Map<Texture, Texture> replacedTextures = new HashMap<Texture, Texture>();
967         for (Enumeration it = offScreenImageUniverse.getLocale().getAllBranchGraphs(); it.hasMoreElements(); ) {
968           cloneTexture((Node)it.nextElement(), replacedTextures);
969         }
970       } else {
971         view = this.offscreenUniverse.getViewer().getView();
972       }
973 
974       updateView(view, this.home.getCamera(), width, height);
975 
976       // Empty temporarily selection to create the off screen image
977       List<Selectable> emptySelection = Collections.emptyList();
978       this.home.setSelectedItems(emptySelection);
979       return Component3DManager.getInstance().getOffScreenImage(view, width, height);
980     } finally {
981       // Restore selection
982       this.home.setSelectedItems(selectedItems);
983       if (offScreenImageUniverse != null) {
984         offScreenImageUniverse.cleanup();
985       }
986     }
987   }
988 
989   /**
990    * Replace the textures set on node shapes by clones.
991    */
cloneTexture(Node node, Map<Texture, Texture> replacedTextures)992   private void cloneTexture(Node node, Map<Texture, Texture> replacedTextures) {
993     if (node instanceof Group) {
994       // Enumerate children
995       Enumeration<?> enumeration = ((Group)node).getAllChildren();
996       while (enumeration.hasMoreElements()) {
997         cloneTexture((Node)enumeration.nextElement(), replacedTextures);
998       }
999     } else if (node instanceof Link) {
1000       cloneTexture(((Link)node).getSharedGroup(), replacedTextures);
1001     } else if (node instanceof Shape3D) {
1002       Appearance appearance = ((Shape3D)node).getAppearance();
1003       if (appearance != null) {
1004         Texture texture = appearance.getTexture();
1005         if (texture != null) {
1006           Texture replacedTexture = replacedTextures.get(texture);
1007           if (replacedTexture == null) {
1008             replacedTexture = (Texture)texture.cloneNodeComponent(false);
1009             replacedTextures.put(texture, replacedTexture);
1010           }
1011           appearance.setTexture(replacedTexture);
1012         }
1013       }
1014     }
1015   }
1016 
1017   /**
1018    * Frees unnecessary resources after the creation of a sequence of multiple offscreen images.
1019    */
endOffscreenImagesCreation()1020   public void endOffscreenImagesCreation() {
1021     if (this.offscreenUniverse != null) {
1022       this.offscreenUniverse.cleanup();
1023       removeHomeListeners();
1024       this.offscreenUniverse = null;
1025     }
1026   }
1027 
1028   /**
1029    * Adds listeners to home to update point of view from current camera.
1030    */
addCameraListeners(final View view, final TransformGroup viewPlatformTransform)1031   private void addCameraListeners(final View view,
1032                                   final TransformGroup viewPlatformTransform) {
1033     this.cameraChangeListener = new PropertyChangeListener() {
1034         private Runnable updater;
1035         public void propertyChange(PropertyChangeEvent ev) {
1036           if (this.updater == null) {
1037             // Update view transform later to avoid flickering in case of multiple camera changes
1038             EventQueue.invokeLater(this.updater = new Runnable () {
1039                 public void run() {
1040                   updateView(view, home.getCamera());
1041                   updateViewPlatformTransform(viewPlatformTransform, home.getCamera(), true);
1042                   updater = null;
1043                 }
1044               });
1045           }
1046         }
1047       };
1048     this.home.getCamera().addPropertyChangeListener(this.cameraChangeListener);
1049     this.homeCameraListener = new PropertyChangeListener() {
1050         public void propertyChange(PropertyChangeEvent ev) {
1051           updateView(view, home.getCamera());
1052           updateViewPlatformTransform(viewPlatformTransform, home.getCamera(), false);
1053           // Add camera change listener to new active camera
1054           ((Camera)ev.getOldValue()).removePropertyChangeListener(cameraChangeListener);
1055           home.getCamera().addPropertyChangeListener(cameraChangeListener);
1056         }
1057       };
1058     this.home.addPropertyChangeListener(Home.Property.CAMERA, this.homeCameraListener);
1059   }
1060 
1061   /**
1062    * Updates <code>view</code> from <code>camera</code> field of view.
1063    */
updateView(View view, Camera camera)1064   private void updateView(View view, Camera camera) {
1065     if (this.component3D != null) {
1066       updateView(view, camera, this.component3D.getWidth(), this.component3D.getHeight());
1067     } else {
1068       updateView(view, camera, 0, 0);
1069     }
1070   }
1071 
updateView(View view, Camera camera, int width, int height)1072   private void updateView(View view, Camera camera, int width, int height) {
1073     float fieldOfView = camera.getFieldOfView();
1074     if (fieldOfView == 0) {
1075       fieldOfView = (float)(Math.PI * 63 / 180);
1076     }
1077     view.setFieldOfView(fieldOfView);
1078     double frontClipDistance = 2.5f;
1079     float frontBackDistanceRatio = 500000; // More than 10 km for a 2.5 cm front distance
1080     if (Component3DManager.getInstance().getDepthSize() <= 16) {
1081       // It's recommended to keep ratio between back and front clip distances under 3000 for a 16 bit Z-buffer
1082       frontBackDistanceRatio = 3000;
1083       BoundingBox approximateHomeBounds = getApproximateHomeBounds();
1084       // If camera is out of home bounds, adjust the front clip distance to the distance to home bounds
1085       if (approximateHomeBounds != null
1086           && !approximateHomeBounds.intersect(new Point3d(camera.getX(), camera.getY(), camera.getZ()))) {
1087         float distanceToClosestBoxSide = getDistanceToBox(camera.getX(), camera.getY(), camera.getZ(), approximateHomeBounds);
1088         if (!Float.isNaN(distanceToClosestBoxSide)) {
1089           frontClipDistance = Math.max(frontClipDistance, 0.1f * distanceToClosestBoxSide);
1090         }
1091       }
1092     }
1093     if (camera.getZ() > 0 && width != 0 && height != 0) {
1094       float halfVerticalFieldOfView = (float)Math.atan(Math.tan(fieldOfView / 2) * height / width);
1095       float fieldOfViewBottomAngle = camera.getPitch() + halfVerticalFieldOfView;
1096       // If the horizon is above the frustrum bottom, take into account the distance to the ground
1097       if (fieldOfViewBottomAngle > 0) {
1098         float distanceToGroundAtFieldOfViewBottomAngle = (float)(camera.getZ() / Math.sin(fieldOfViewBottomAngle));
1099         frontClipDistance = Math.min(frontClipDistance, 0.35f * distanceToGroundAtFieldOfViewBottomAngle);
1100         if (frontClipDistance * frontBackDistanceRatio < distanceToGroundAtFieldOfViewBottomAngle) {
1101           // Ensure the ground is always visible at the back clip distance
1102           frontClipDistance = distanceToGroundAtFieldOfViewBottomAngle / frontBackDistanceRatio;
1103         }
1104       }
1105     }
1106     // Update front and back clip distance
1107     view.setFrontClipDistance(frontClipDistance);
1108     view.setBackClipDistance(frontClipDistance * frontBackDistanceRatio);
1109     clearPrintedImageCache();
1110   }
1111 
1112   /**
1113    * Returns quickly computed bounds of the objects in home.
1114    */
getApproximateHomeBounds()1115   private BoundingBox getApproximateHomeBounds() {
1116     if (this.approximateHomeBoundsCache == null) {
1117       BoundingBox approximateHomeBounds = null;
1118       for (HomePieceOfFurniture piece : this.home.getFurniture()) {
1119         if (piece.isVisible()
1120             && (piece.getLevel() == null
1121                 || piece.getLevel().isViewable())) {
1122           float halfMaxDimension = Math.max(piece.getWidthInPlan(), piece.getDepthInPlan()) / 2;
1123           float elevation = piece.getGroundElevation();
1124           Point3d pieceLocation = new Point3d(
1125               piece.getX() - halfMaxDimension, piece.getY() - halfMaxDimension, elevation);
1126           if (approximateHomeBounds == null) {
1127             approximateHomeBounds = new BoundingBox(pieceLocation, pieceLocation);
1128           } else {
1129             approximateHomeBounds.combine(pieceLocation);
1130           }
1131           approximateHomeBounds.combine(new Point3d(
1132               piece.getX() + halfMaxDimension, piece.getY() + halfMaxDimension, elevation + piece.getHeightInPlan()));
1133         }
1134       }
1135       for (Wall wall : this.home.getWalls()) {
1136         if (wall.getLevel() == null
1137             || wall.getLevel().isViewable()) {
1138           Point3d startPoint = new Point3d(wall.getXStart(), wall.getYStart(),
1139               wall.getLevel() != null ? wall.getLevel().getElevation() : 0);
1140           if (approximateHomeBounds == null) {
1141             approximateHomeBounds = new BoundingBox(startPoint, startPoint);
1142           } else {
1143             approximateHomeBounds.combine(startPoint);
1144           }
1145           approximateHomeBounds.combine(new Point3d(wall.getXEnd(), wall.getYEnd(),
1146               startPoint.z + (wall.getHeight() != null ? wall.getHeight() : this.home.getWallHeight())));
1147         }
1148       }
1149       for (Room room : this.home.getRooms()) {
1150         if (room.getLevel() == null
1151             || room.getLevel().isViewable()) {
1152           Point3d center = new Point3d(room.getXCenter(), room.getYCenter(),
1153               room.getLevel() != null ? room.getLevel().getElevation() : 0);
1154           if (approximateHomeBounds == null) {
1155             approximateHomeBounds = new BoundingBox(center, center);
1156           } else {
1157             approximateHomeBounds.combine(center);
1158           }
1159         }
1160       }
1161       for (Label label : this.home.getLabels()) {
1162         if ((label.getLevel() == null
1163               || label.getLevel().isViewable())
1164             && label.getPitch() != null) {
1165           Point3d center = new Point3d(label.getX(), label.getY(), label.getGroundElevation());
1166           if (approximateHomeBounds == null) {
1167             approximateHomeBounds = new BoundingBox(center, center);
1168           } else {
1169             approximateHomeBounds.combine(center);
1170           }
1171         }
1172       }
1173       this.approximateHomeBoundsCache = approximateHomeBounds;
1174     }
1175     return this.approximateHomeBoundsCache;
1176   }
1177 
1178   /**
1179    * Returns the distance between the point at the given coordinates (x,y,z) and the closest side of <code>box</code>.
1180    */
getDistanceToBox(float x, float y, float z, BoundingBox box)1181   private float getDistanceToBox(float x, float y, float z, BoundingBox box) {
1182     Point3f point = new Point3f(x, y, z);
1183     Point3d lower = new Point3d();
1184     box.getLower(lower);
1185     Point3d upper = new Point3d();
1186     box.getUpper(upper);
1187     Point3f [] boxVertices = {
1188       new Point3f((float)lower.x, (float)lower.y, (float)lower.z),
1189       new Point3f((float)upper.x, (float)lower.y, (float)lower.z),
1190       new Point3f((float)lower.x, (float)upper.y, (float)lower.z),
1191       new Point3f((float)upper.x, (float)upper.y, (float)lower.z),
1192       new Point3f((float)lower.x, (float)lower.y, (float)upper.z),
1193       new Point3f((float)upper.x, (float)lower.y, (float)upper.z),
1194       new Point3f((float)lower.x, (float)upper.y, (float)upper.z),
1195       new Point3f((float)upper.x, (float)upper.y, (float)upper.z)};
1196     float [] distancesToVertex = new float [boxVertices.length];
1197     for (int i = 0; i < distancesToVertex.length; i++) {
1198       distancesToVertex [i] = point.distanceSquared(boxVertices [i]);
1199     }
1200     float [] distancesToSide = {
1201         getDistanceToSide(point, boxVertices, distancesToVertex, 0, 1, 3, 2, 2),
1202         getDistanceToSide(point, boxVertices, distancesToVertex, 0, 1, 5, 4, 1),
1203         getDistanceToSide(point, boxVertices, distancesToVertex, 0, 2, 6, 4, 0),
1204         getDistanceToSide(point, boxVertices, distancesToVertex, 4, 5, 7, 6, 2),
1205         getDistanceToSide(point, boxVertices, distancesToVertex, 2, 3, 7, 6, 1),
1206         getDistanceToSide(point, boxVertices, distancesToVertex, 1, 3, 7, 5, 0)};
1207     float distance = distancesToSide [0];
1208     for (int i = 1; i < distancesToSide.length; i++) {
1209       distance = Math.min(distance, distancesToSide [i]);
1210     }
1211     return distance;
1212   }
1213 
1214   /**
1215    * Returns the distance between the given <code>point</code> and the plane defined by four vertices.
1216    */
getDistanceToSide(Point3f point, Point3f [] boxVertices, float [] distancesSquaredToVertex, int index1, int index2, int index3, int index4, int axis)1217   private float getDistanceToSide(Point3f point, Point3f [] boxVertices, float [] distancesSquaredToVertex,
1218                                   int index1, int index2, int index3, int index4, int axis) {
1219     switch (axis) {
1220       case 0 : // Normal along x axis
1221         if (point.y <= boxVertices [index1].y) {
1222           if (point.z <= boxVertices [index1].z) {
1223             return (float)Math.sqrt(distancesSquaredToVertex [index1]);
1224           } else if (point.z >= boxVertices [index4].z) {
1225             return (float)Math.sqrt(distancesSquaredToVertex [index4]);
1226           } else {
1227             return getDistanceToLine(point, boxVertices [index1], boxVertices [index4]);
1228           }
1229         } else if (point.y >= boxVertices [index2].y) {
1230           if (point.z <= boxVertices [index2].z) {
1231             return (float)Math.sqrt(distancesSquaredToVertex [index2]);
1232           } else if (point.z >= boxVertices [index3].z) {
1233             return (float)Math.sqrt(distancesSquaredToVertex [index3]);
1234           } else {
1235             return getDistanceToLine(point, boxVertices [index2], boxVertices [index3]);
1236           }
1237         } else if (point.z <= boxVertices [index1].z) {
1238           return getDistanceToLine(point, boxVertices [index1], boxVertices [index2]);
1239         } else if (point.z >= boxVertices [index4].z) {
1240           return getDistanceToLine(point, boxVertices [index3], boxVertices [index4]);
1241         }
1242         break;
1243       case 1 : // Normal along y axis
1244         if (point.x <= boxVertices [index1].x) {
1245           if (point.z <= boxVertices [index1].z) {
1246             return (float)Math.sqrt(distancesSquaredToVertex [index1]);
1247           } else if (point.z >= boxVertices [index4].z) {
1248             return (float)Math.sqrt(distancesSquaredToVertex [index4]);
1249           } else {
1250             return getDistanceToLine(point, boxVertices [index1], boxVertices [index4]);
1251           }
1252         } else if (point.x >= boxVertices [index2].x) {
1253           if (point.z <= boxVertices [index2].z) {
1254             return (float)Math.sqrt(distancesSquaredToVertex [index2]);
1255           } else if (point.z >= boxVertices [index3].z) {
1256             return (float)Math.sqrt(distancesSquaredToVertex [index3]);
1257           } else {
1258             return getDistanceToLine(point, boxVertices [index2], boxVertices [index3]);
1259           }
1260         } else if (point.z <= boxVertices [index1].z) {
1261           return getDistanceToLine(point, boxVertices [index1], boxVertices [index2]);
1262         } else if (point.z >= boxVertices [index4].z) {
1263           return getDistanceToLine(point, boxVertices [index3], boxVertices [index4]);
1264         }
1265         break;
1266       case 2 : // Normal along z axis
1267         if (point.x <= boxVertices [index1].x) {
1268           if (point.y <= boxVertices [index1].y) {
1269             return (float)Math.sqrt(distancesSquaredToVertex [index1]);
1270           } else if (point.y >= boxVertices [index4].y) {
1271             return (float)Math.sqrt(distancesSquaredToVertex [index4]);
1272           } else {
1273             return getDistanceToLine(point, boxVertices [index1], boxVertices [index4]);
1274           }
1275         } else if (point.x >= boxVertices [index2].x) {
1276           if (point.y <= boxVertices [index2].y) {
1277             return (float)Math.sqrt(distancesSquaredToVertex [index2]);
1278           } else if (point.y >= boxVertices [index3].y) {
1279             return (float)Math.sqrt(distancesSquaredToVertex [index3]);
1280           } else {
1281             return getDistanceToLine(point, boxVertices [index2], boxVertices [index3]);
1282           }
1283         } else if (point.y <= boxVertices [index1].y) {
1284           return getDistanceToLine(point, boxVertices [index1], boxVertices [index2]);
1285         } else if (point.y >= boxVertices [index4].y) {
1286           return getDistanceToLine(point, boxVertices [index3], boxVertices [index4]);
1287         }
1288         break;
1289     }
1290 
1291     // Return distance to plane
1292     // from https://fr.wikipedia.org/wiki/Distance_d%27un_point_�_un_plan
1293     Vector3f vector1 = new Vector3f(boxVertices [index2].x - boxVertices [index1].x,
1294         boxVertices [index2].y - boxVertices [index1].y,
1295         boxVertices [index2].z - boxVertices [index1].z);
1296     Vector3f vector2 = new Vector3f(boxVertices [index3].x - boxVertices [index1].x,
1297         boxVertices [index3].y - boxVertices [index1].y,
1298         boxVertices [index3].z - boxVertices [index1].z);
1299     Vector3f normal = new Vector3f();
1300     normal.cross(vector1, vector2);
1301     return Math.abs(normal.dot(new Vector3f(boxVertices [index1].x - point.x, boxVertices [index1].y - point.y, boxVertices [index1].z - point.z))) /
1302         normal.length();
1303   }
1304 
1305   /**
1306    * Returns the distance between the given <code>point</code> and the line defined by two points.
1307    */
getDistanceToLine(Point3f point, Point3f point1, Point3f point2)1308   private float getDistanceToLine(Point3f point, Point3f point1, Point3f point2) {
1309     // From https://fr.wikipedia.org/wiki/Distance_d%27un_point_�_une_droite#Dans_l.27espace
1310     Vector3f lineDirection = new Vector3f(point2.x - point1.x, point2.y - point1.y, point2.z - point1.z);
1311     Vector3f vector = new Vector3f(point.x - point1.x, point.y - point1.y, point.z - point1.z);
1312     Vector3f crossProduct = new Vector3f();
1313     crossProduct.cross(lineDirection, vector);
1314     return crossProduct.length() / lineDirection.length();
1315   }
1316 
1317   /**
1318    * Frees printed image kept in cache.
1319    */
clearPrintedImageCache()1320   private void clearPrintedImageCache() {
1321     this.printedImageCache = null;
1322   }
1323 
1324   /**
1325    * Updates <code>viewPlatformTransform</code> transform from <code>camera</code> angles and location.
1326    */
updateViewPlatformTransform(TransformGroup viewPlatformTransform, Camera camera, boolean updateWithAnimation)1327   private void updateViewPlatformTransform(TransformGroup viewPlatformTransform,
1328                                            Camera camera, boolean updateWithAnimation) {
1329     // Get the camera interpolator
1330     CameraInterpolator cameraInterpolator =
1331         (CameraInterpolator)viewPlatformTransform.getChild(viewPlatformTransform.numChildren() - 1);
1332     if (updateWithAnimation) {
1333       cameraInterpolator.moveCamera(camera);
1334     } else {
1335       cameraInterpolator.stop();
1336       Transform3D transform = new Transform3D();
1337       updateViewPlatformTransform(transform, camera.getX(), camera.getY(),
1338           camera.getZ(), camera.getYaw(), camera.getPitch());
1339       viewPlatformTransform.setTransform(transform);
1340     }
1341     clearPrintedImageCache();
1342   }
1343 
1344   /**
1345    * An interpolator that computes smooth camera moves.
1346    */
1347   private class CameraInterpolator extends TransformInterpolator {
1348     private final ScheduledExecutorService scheduledExecutor;
1349     private Camera initialCamera;
1350     private Camera finalCamera;
1351 
CameraInterpolator(TransformGroup transformGroup)1352     public CameraInterpolator(TransformGroup transformGroup) {
1353       this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
1354       setTarget(transformGroup);
1355     }
1356 
1357     /**
1358      * Moves the camera to a new location.
1359      */
moveCamera(Camera finalCamera)1360     public void moveCamera(Camera finalCamera) {
1361       if (this.finalCamera == null
1362           || this.finalCamera.getX() != finalCamera.getX()
1363           || this.finalCamera.getY() != finalCamera.getY()
1364           || this.finalCamera.getZ() != finalCamera.getZ()
1365           || this.finalCamera.getYaw() != finalCamera.getYaw()
1366           || this.finalCamera.getPitch() != finalCamera.getPitch()) {
1367         synchronized (this) {
1368           Alpha alpha = getAlpha();
1369           if (alpha == null || alpha.finished()) {
1370             this.initialCamera = new Camera(camera.getX(), camera.getY(), camera.getZ(),
1371                 camera.getYaw(), camera.getPitch(), camera.getFieldOfView());
1372           } else if (alpha.value() < 0.3) {
1373             Transform3D finalTransformation = new Transform3D();
1374             // Jump directly to final location
1375             updateViewPlatformTransform(finalTransformation, this.finalCamera.getX(), this.finalCamera.getY(), this.finalCamera.getZ(),
1376                 this.finalCamera.getYaw(), this.finalCamera.getPitch());
1377             getTarget().setTransform(finalTransformation);
1378             this.initialCamera = this.finalCamera;
1379           } else {
1380             // Compute initial location from current alpha value
1381             this.initialCamera = new Camera(this.initialCamera.getX() + (this.finalCamera.getX() - this.initialCamera.getX()) * alpha.value(),
1382                 this.initialCamera.getY() + (this.finalCamera.getY() - this.initialCamera.getY()) * alpha.value(),
1383                 this.initialCamera.getZ() + (this.finalCamera.getZ() - this.initialCamera.getZ()) * alpha.value(),
1384                 this.initialCamera.getYaw() + (this.finalCamera.getYaw() - this.initialCamera.getYaw()) * alpha.value(),
1385                 this.initialCamera.getPitch() + (this.finalCamera.getPitch() - this.initialCamera.getPitch()) * alpha.value(),
1386                 finalCamera.getFieldOfView());
1387           }
1388           this.finalCamera = new Camera(finalCamera.getX(), finalCamera.getY(), finalCamera.getZ(),
1389               finalCamera.getYaw(), finalCamera.getPitch(), finalCamera.getFieldOfView());
1390 
1391           // Create an animation that will interpolate camera location
1392           // between initial camera and final camera in 150 ms
1393           if (alpha == null) {
1394             alpha = new Alpha(1, 150);
1395             setAlpha(alpha);
1396           }
1397           // Start animation now
1398           alpha.setStartTime(System.currentTimeMillis());
1399           // In case system is overloaded computeTransform won't be called
1400           // ensure final location will always be set after 150 ms
1401           this.scheduledExecutor.schedule(new Runnable() {
1402               public void run() {
1403                 if (getAlpha().value() == 1) {
1404                   Transform3D transform = new Transform3D();
1405                   computeTransform(1, transform);
1406                   getTarget().setTransform(transform);
1407                 }
1408               }
1409             }, 150, TimeUnit.MILLISECONDS);
1410         }
1411       }
1412     }
1413 
1414     @Override
computeTransform(float alpha, Transform3D transform)1415     public synchronized void computeTransform(float alpha, Transform3D transform) {
1416       updateViewPlatformTransform(transform,
1417           this.initialCamera.getX() + (this.finalCamera.getX() - this.initialCamera.getX()) * alpha,
1418           this.initialCamera.getY() + (this.finalCamera.getY() - this.initialCamera.getY()) * alpha,
1419           this.initialCamera.getZ() + (this.finalCamera.getZ() - this.initialCamera.getZ()) * alpha,
1420           this.initialCamera.getYaw() + (this.finalCamera.getYaw() - this.initialCamera.getYaw()) * alpha,
1421           this.initialCamera.getPitch() + (this.finalCamera.getPitch() - this.initialCamera.getPitch()) * alpha);
1422     }
1423 
stop()1424     public synchronized void stop() {
1425       setAlpha(null);
1426       this.finalCamera = null;
1427     }
1428   }
1429 
1430   /**
1431    * Updates <code>viewPlatformTransform</code> transform from camera angles and location.
1432    */
updateViewPlatformTransform(Transform3D transform, float cameraX, float cameraY, float cameraZ, float cameraYaw, float cameraPitch)1433   private void updateViewPlatformTransform(Transform3D transform,
1434                                            float cameraX, float cameraY, float cameraZ,
1435                                            float cameraYaw, float cameraPitch) {
1436     Transform3D yawRotation = new Transform3D();
1437     yawRotation.rotY(-cameraYaw + Math.PI);
1438 
1439     Transform3D pitchRotation = new Transform3D();
1440     pitchRotation.rotX(-cameraPitch);
1441     yawRotation.mul(pitchRotation);
1442 
1443     transform.setIdentity();
1444     transform.setTranslation(new Vector3f(cameraX, cameraZ, cameraY));
1445     transform.mul(yawRotation);
1446 
1447     this.camera = new Camera(cameraX, cameraY, cameraZ, cameraYaw, cameraPitch, 0);
1448   }
1449 
1450   /**
1451    * Adds AWT mouse listeners to <code>component3D</code> that calls back <code>controller</code> methods.
1452    */
addMouseListeners(final HomeController3D controller, final Component component3D)1453   private void addMouseListeners(final HomeController3D controller, final Component component3D) {
1454     MouseInputAdapter mouseListener = new MouseInputAdapter() {
1455         private int        xLastMouseMove;
1456         private int        yLastMouseMove;
1457         private Component  grabComponent;
1458         private Component  previousMouseEventTarget;
1459 
1460         @Override
1461         public void mousePressed(MouseEvent ev) {
1462           if (!retargetMouseEventToNavigationPanelChildren(ev)) {
1463             if (ev.isPopupTrigger()) {
1464               mouseReleased(ev);
1465             } else if (isEnabled()) {
1466               requestFocusInWindow();
1467               this.xLastMouseMove = ev.getX();
1468               this.yLastMouseMove = ev.getY();
1469             }
1470           }
1471         }
1472 
1473         @Override
1474         public void mouseClicked(MouseEvent ev) {
1475           retargetMouseEventToNavigationPanelChildren(ev);
1476         }
1477 
1478         @Override
1479         public void mouseMoved(MouseEvent ev) {
1480           retargetMouseEventToNavigationPanelChildren(ev);
1481         }
1482 
1483         @Override
1484         public void mouseDragged(MouseEvent ev) {
1485           if (!retargetMouseEventToNavigationPanelChildren(ev)) {
1486             if (isEnabled()) {
1487               if (ev.isAltDown()) {
1488                 // Mouse move along Y axis while alt is down changes camera location
1489                 float delta = 1.25f * (this.yLastMouseMove - ev.getY());
1490                 // Multiply delta by 5 if shift is down
1491                 if (ev.isShiftDown()) {
1492                   delta *= 5;
1493                 }
1494                 controller.moveCamera(delta);
1495               } else {
1496                 final float ANGLE_FACTOR = 0.005f;
1497                 // Mouse move along X axis changes camera yaw
1498                 float yawDelta = ANGLE_FACTOR * (ev.getX() - this.xLastMouseMove);
1499                 // Multiply yaw delta by 5 if shift is down
1500                 if (ev.isShiftDown()) {
1501                   yawDelta *= 5;
1502                 }
1503                 controller.rotateCameraYaw(yawDelta);
1504 
1505                 // Mouse move along Y axis changes camera pitch
1506                 float pitchDelta = ANGLE_FACTOR * (ev.getY() - this.yLastMouseMove);
1507                 controller.rotateCameraPitch(pitchDelta);
1508               }
1509 
1510               this.xLastMouseMove = ev.getX();
1511               this.yLastMouseMove = ev.getY();
1512             }
1513           }
1514         }
1515 
1516         @Override
1517         public void mouseReleased(MouseEvent ev) {
1518           if (!retargetMouseEventToNavigationPanelChildren(ev)) {
1519             if (ev.isPopupTrigger()) {
1520               JPopupMenu componentPopupMenu = getComponentPopupMenu();
1521               if (componentPopupMenu != null) {
1522                 componentPopupMenu.show(HomeComponent3D.this, ev.getX(), ev.getY());
1523               }
1524             }
1525           }
1526         }
1527 
1528         /**
1529          * Retargets to the first component of navigation panel able to manage the given event
1530          * and returns <code>true</code> if a component consumed the event
1531          * or needs to be repainted (meaning its state changed).
1532          * This implementation doesn't cover all the possible cases (mouseEntered and mouseExited
1533          * events are managed only during mouseDragged event).
1534          */
1535         private boolean retargetMouseEventToNavigationPanelChildren(MouseEvent ev) {
1536           if (navigationPanel != null
1537               && navigationPanel.isVisible()) {
1538             if (this.grabComponent != null
1539                 && (ev.getID() == MouseEvent.MOUSE_RELEASED
1540                     || ev.getID() == MouseEvent.MOUSE_DRAGGED)) {
1541               Point point = SwingUtilities.convertPoint(ev.getComponent(), ev.getPoint(), this.grabComponent);
1542               dispatchRetargetedEvent(deriveEvent(ev, this.grabComponent, ev.getID(), point.x, point.y));
1543               if (ev.getID() == MouseEvent.MOUSE_RELEASED) {
1544                 this.grabComponent = null;
1545               } else {
1546                 if (this.previousMouseEventTarget == null
1547                     && this.grabComponent.contains(point)) {
1548                   dispatchRetargetedEvent(deriveEvent(ev, this.grabComponent, MouseEvent.MOUSE_ENTERED, point.x, point.y));
1549                   this.previousMouseEventTarget = this.grabComponent;
1550                 } else if (this.previousMouseEventTarget != null
1551                     && !this.grabComponent.contains(point)) {
1552                   dispatchRetargetedEvent(deriveEvent(ev, this.grabComponent, MouseEvent.MOUSE_EXITED, point.x, point.y));
1553                   this.previousMouseEventTarget = null;
1554                 }
1555               }
1556               return true;
1557             } else {
1558               Component mouseEventTarget = retargetMouseEvent(navigationPanel, ev);
1559               if (mouseEventTarget != null) {
1560                 this.previousMouseEventTarget = mouseEventTarget;
1561                 return true;
1562               }
1563             }
1564           }
1565           return false;
1566         }
1567 
1568         private Component retargetMouseEvent(Component component, MouseEvent ev) {
1569           if (component.getBounds().contains(ev.getPoint())) {
1570             if (component instanceof Container) {
1571               Container container = (Container)component;
1572               for (int i = container.getComponentCount() - 1; i >= 0; i--) {
1573                 Component c = container.getComponent(i);
1574                 MouseEvent retargetedEvent = deriveEvent(ev, component, ev.getID(),
1575                     ev.getX() - component.getX(), ev.getY() - component.getY());
1576                 Component mouseEventTarget = retargetMouseEvent(c, retargetedEvent);
1577                 if (mouseEventTarget != null) {
1578                   return mouseEventTarget;
1579                 }
1580               }
1581             }
1582             int newX = ev.getX() - component.getX();
1583             int newY = ev.getY() - component.getY();
1584             if (dispatchRetargetedEvent(deriveEvent(ev, component, ev.getID(), newX, newY))) {
1585               if (ev.getID() == MouseEvent.MOUSE_PRESSED) {
1586                 this.grabComponent = component;
1587               }
1588               return component;
1589             }
1590           }
1591           return null;
1592         }
1593 
1594         /**
1595          * Dispatches the given event to its component and returns <code>true</code> if component needs to be redrawn.
1596          */
1597         private boolean dispatchRetargetedEvent(MouseEvent ev) {
1598           ev.getComponent().dispatchEvent(ev);
1599           if (!RepaintManager.currentManager(ev.getComponent()).getDirtyRegion((JComponent)ev.getComponent()).isEmpty()) {
1600             updateNavigationPanelImage();
1601             component3D.repaint();
1602             return true;
1603           }
1604           return false;
1605         }
1606 
1607         /**
1608          * Returns a new <code>MouseEvent</code> derived from the one given in parameter.
1609          */
1610         private MouseEvent deriveEvent(MouseEvent ev, Component component, int id, int x, int y) {
1611           return new MouseEvent(component, id, ev.getWhen(),
1612               ev.getModifiersEx() | ev.getModifiers(), x, y,
1613               ev.getClickCount(), ev.isPopupTrigger(), ev.getButton());
1614         }
1615       };
1616     MouseWheelListener mouseWheelListener = new MouseWheelListener() {
1617         public void mouseWheelMoved(MouseWheelEvent ev) {
1618           if (isEnabled()) {
1619             // Mouse wheel changes camera location
1620             float delta = -2.5f * ev.getWheelRotation();
1621             // Multiply delta by 10 if shift is down
1622             if (ev.isShiftDown()) {
1623               delta *= 5;
1624             }
1625             controller.moveCamera(delta);
1626           }
1627         }
1628       };
1629 
1630     component3D.addMouseListener(mouseListener);
1631     component3D.addMouseMotionListener(mouseListener);
1632     component3D.addMouseWheelListener(mouseWheelListener);
1633     // Add a mouse listener to this component to request focus in case user clicks in component border
1634     super.addMouseListener(new MouseInputAdapter() {
1635         @Override
1636         public void mousePressed(MouseEvent e) {
1637           requestFocusInWindow();
1638         }
1639       });
1640   }
1641 
1642   /**
1643    * Installs keys bound to actions.
1644    */
installKeyboardActions()1645   private void installKeyboardActions() {
1646     InputMap inputMap = getInputMap(WHEN_FOCUSED);
1647     // Tolerate alt modifier for forward and backward moves with UP and DOWN keys to avoid
1648     // the user to release the alt key when he wants to alternate forward/backward and sideways moves
1649     inputMap.put(KeyStroke.getKeyStroke("shift UP"), ActionType.MOVE_CAMERA_FAST_FORWARD);
1650     inputMap.put(KeyStroke.getKeyStroke("shift alt UP"), ActionType.MOVE_CAMERA_FAST_FORWARD);
1651     inputMap.put(KeyStroke.getKeyStroke("shift W"), ActionType.MOVE_CAMERA_FAST_FORWARD);
1652     inputMap.put(KeyStroke.getKeyStroke("UP"), ActionType.MOVE_CAMERA_FORWARD);
1653     inputMap.put(KeyStroke.getKeyStroke("alt UP"), ActionType.MOVE_CAMERA_FORWARD);
1654     inputMap.put(KeyStroke.getKeyStroke("W"), ActionType.MOVE_CAMERA_FORWARD);
1655     inputMap.put(KeyStroke.getKeyStroke("shift DOWN"), ActionType.MOVE_CAMERA_FAST_BACKWARD);
1656     inputMap.put(KeyStroke.getKeyStroke("shift alt DOWN"), ActionType.MOVE_CAMERA_FAST_BACKWARD);
1657     inputMap.put(KeyStroke.getKeyStroke("shift S"), ActionType.MOVE_CAMERA_FAST_BACKWARD);
1658     inputMap.put(KeyStroke.getKeyStroke("DOWN"), ActionType.MOVE_CAMERA_BACKWARD);
1659     inputMap.put(KeyStroke.getKeyStroke("alt DOWN"), ActionType.MOVE_CAMERA_BACKWARD);
1660     inputMap.put(KeyStroke.getKeyStroke("S"), ActionType.MOVE_CAMERA_BACKWARD);
1661     inputMap.put(KeyStroke.getKeyStroke("shift alt LEFT"), ActionType.MOVE_CAMERA_FAST_LEFT);
1662     inputMap.put(KeyStroke.getKeyStroke("alt LEFT"), ActionType.MOVE_CAMERA_LEFT);
1663     inputMap.put(KeyStroke.getKeyStroke("shift alt RIGHT"), ActionType.MOVE_CAMERA_FAST_RIGHT);
1664     inputMap.put(KeyStroke.getKeyStroke("alt RIGHT"), ActionType.MOVE_CAMERA_RIGHT);
1665     inputMap.put(KeyStroke.getKeyStroke("shift LEFT"), ActionType.ROTATE_CAMERA_YAW_FAST_LEFT);
1666     inputMap.put(KeyStroke.getKeyStroke("shift A"), ActionType.ROTATE_CAMERA_YAW_FAST_LEFT);
1667     inputMap.put(KeyStroke.getKeyStroke("LEFT"), ActionType.ROTATE_CAMERA_YAW_LEFT);
1668     inputMap.put(KeyStroke.getKeyStroke("A"), ActionType.ROTATE_CAMERA_YAW_LEFT);
1669     inputMap.put(KeyStroke.getKeyStroke("shift RIGHT"), ActionType.ROTATE_CAMERA_YAW_FAST_RIGHT);
1670     inputMap.put(KeyStroke.getKeyStroke("shift D"), ActionType.ROTATE_CAMERA_YAW_FAST_RIGHT);
1671     inputMap.put(KeyStroke.getKeyStroke("RIGHT"), ActionType.ROTATE_CAMERA_YAW_RIGHT);
1672     inputMap.put(KeyStroke.getKeyStroke("D"), ActionType.ROTATE_CAMERA_YAW_RIGHT);
1673     inputMap.put(KeyStroke.getKeyStroke("shift PAGE_UP"), ActionType.ROTATE_CAMERA_PITCH_FAST_UP);
1674     inputMap.put(KeyStroke.getKeyStroke("PAGE_UP"), ActionType.ROTATE_CAMERA_PITCH_UP);
1675     inputMap.put(KeyStroke.getKeyStroke("shift PAGE_DOWN"), ActionType.ROTATE_CAMERA_PITCH_FAST_DOWN);
1676     inputMap.put(KeyStroke.getKeyStroke("PAGE_DOWN"), ActionType.ROTATE_CAMERA_PITCH_DOWN);
1677     inputMap.put(KeyStroke.getKeyStroke("shift HOME"), ActionType.ELEVATE_CAMERA_FAST_UP);
1678     inputMap.put(KeyStroke.getKeyStroke("HOME"), ActionType.ELEVATE_CAMERA_UP);
1679     inputMap.put(KeyStroke.getKeyStroke("shift END"), ActionType.ELEVATE_CAMERA_FAST_DOWN);
1680     inputMap.put(KeyStroke.getKeyStroke("END"), ActionType.ELEVATE_CAMERA_DOWN);
1681   }
1682 
1683   /**
1684    * Creates actions that calls back <code>controller</code> methods.
1685    */
createActions(final HomeController3D controller)1686   private void createActions(final HomeController3D controller) {
1687     // Move camera action mapped to arrow keys
1688     class MoveCameraAction extends AbstractAction {
1689       private final float delta;
1690 
1691       public MoveCameraAction(float delta) {
1692         this.delta = delta;
1693       }
1694 
1695       public void actionPerformed(ActionEvent e) {
1696         controller.moveCamera(this.delta);
1697       }
1698     }
1699     // Move camera sideways action mapped to arrow keys
1700     class MoveCameraSidewaysAction extends AbstractAction {
1701       private final float delta;
1702 
1703       public MoveCameraSidewaysAction(float delta) {
1704         this.delta = delta;
1705       }
1706 
1707       public void actionPerformed(ActionEvent e) {
1708         controller.moveCameraSideways(this.delta);
1709       }
1710     }
1711     // Elevate camera action mapped to arrow keys
1712     class ElevateCameraAction extends AbstractAction {
1713       private final float delta;
1714 
1715       public ElevateCameraAction(float delta) {
1716         this.delta = delta;
1717       }
1718 
1719       public void actionPerformed(ActionEvent e) {
1720         controller.elevateCamera(this.delta);
1721       }
1722     }
1723     // Rotate camera yaw action mapped to arrow keys
1724     class RotateCameraYawAction extends AbstractAction {
1725       private final float delta;
1726 
1727       public RotateCameraYawAction(float delta) {
1728         this.delta = delta;
1729       }
1730 
1731       public void actionPerformed(ActionEvent e) {
1732         controller.rotateCameraYaw(this.delta);
1733       }
1734     }
1735     // Rotate camera pitch action mapped to arrow keys
1736     class RotateCameraPitchAction extends AbstractAction {
1737       private final float delta;
1738 
1739       public RotateCameraPitchAction(float delta) {
1740         this.delta = delta;
1741       }
1742 
1743       public void actionPerformed(ActionEvent e) {
1744         controller.rotateCameraPitch(this.delta);
1745       }
1746     }
1747     ActionMap actionMap = getActionMap();
1748     actionMap.put(ActionType.MOVE_CAMERA_FORWARD, new MoveCameraAction(6.5f));
1749     actionMap.put(ActionType.MOVE_CAMERA_FAST_FORWARD, new MoveCameraAction(32.5f));
1750     actionMap.put(ActionType.MOVE_CAMERA_BACKWARD, new MoveCameraAction(-6.5f));
1751     actionMap.put(ActionType.MOVE_CAMERA_FAST_BACKWARD, new MoveCameraAction(-32.5f));
1752     actionMap.put(ActionType.MOVE_CAMERA_LEFT, new MoveCameraSidewaysAction(-2.5f));
1753     actionMap.put(ActionType.MOVE_CAMERA_FAST_LEFT, new MoveCameraSidewaysAction(-10f));
1754     actionMap.put(ActionType.MOVE_CAMERA_RIGHT, new MoveCameraSidewaysAction(2.5f));
1755     actionMap.put(ActionType.MOVE_CAMERA_FAST_RIGHT, new MoveCameraSidewaysAction(10f));
1756     actionMap.put(ActionType.ELEVATE_CAMERA_DOWN, new ElevateCameraAction(-2.5f));
1757     actionMap.put(ActionType.ELEVATE_CAMERA_FAST_DOWN, new ElevateCameraAction(-10f));
1758     actionMap.put(ActionType.ELEVATE_CAMERA_UP, new ElevateCameraAction(2.5f));
1759     actionMap.put(ActionType.ELEVATE_CAMERA_FAST_UP, new ElevateCameraAction(10f));
1760     actionMap.put(ActionType.ROTATE_CAMERA_YAW_LEFT, new RotateCameraYawAction(-(float)Math.PI / 60));
1761     actionMap.put(ActionType.ROTATE_CAMERA_YAW_FAST_LEFT, new RotateCameraYawAction(-(float)Math.PI / 12));
1762     actionMap.put(ActionType.ROTATE_CAMERA_YAW_RIGHT, new RotateCameraYawAction((float)Math.PI / 60));
1763     actionMap.put(ActionType.ROTATE_CAMERA_YAW_FAST_RIGHT, new RotateCameraYawAction((float)Math.PI / 12));
1764     actionMap.put(ActionType.ROTATE_CAMERA_PITCH_UP, new RotateCameraPitchAction(-(float)Math.PI / 120));
1765     actionMap.put(ActionType.ROTATE_CAMERA_PITCH_FAST_UP, new RotateCameraPitchAction(-(float)Math.PI / 24));
1766     actionMap.put(ActionType.ROTATE_CAMERA_PITCH_DOWN, new RotateCameraPitchAction((float)Math.PI / 120));
1767     actionMap.put(ActionType.ROTATE_CAMERA_PITCH_FAST_DOWN, new RotateCameraPitchAction((float)Math.PI / 24));
1768   }
1769 
1770   @Override
addMouseMotionListener(final MouseMotionListener l)1771   public void addMouseMotionListener(final MouseMotionListener l) {
1772     super.addMouseMotionListener(l);
1773     if (this.component3D != null) {
1774       this.component3D.addMouseMotionListener(new MouseMotionListener() {
1775           public void mouseMoved(MouseEvent ev) {
1776             l.mouseMoved(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this));
1777           }
1778 
1779           public void mouseDragged(MouseEvent ev) {
1780             l.mouseDragged(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this));
1781           }
1782         });
1783     }
1784   }
1785 
1786   @Override
removeMouseMotionListener(final MouseMotionListener l)1787   public void removeMouseMotionListener(final MouseMotionListener l) {
1788     if (this.component3D != null) {
1789       this.component3D.removeMouseMotionListener(l);
1790     }
1791     super.removeMouseMotionListener(l);
1792   }
1793 
1794   @Override
addMouseListener(final MouseListener l)1795   public void addMouseListener(final MouseListener l) {
1796     super.addMouseListener(l);
1797     if (this.component3D != null) {
1798       this.component3D.addMouseListener(new MouseListener() {
1799           public void mousePressed(MouseEvent ev) {
1800             l.mousePressed(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this));
1801           }
1802 
1803           public void mouseClicked(MouseEvent ev) {
1804             l.mouseClicked(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this));
1805           }
1806 
1807           public void mouseReleased(MouseEvent ev) {
1808             l.mouseReleased(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this));
1809           }
1810 
1811           public void mouseExited(MouseEvent ev) {
1812             l.mouseExited(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this));
1813           }
1814 
1815           public void mouseEntered(MouseEvent ev) {
1816             l.mouseEntered(SwingUtilities.convertMouseEvent(component3D, ev, HomeComponent3D.this));
1817           }
1818         });
1819     }
1820   }
1821 
1822   @Override
removeMouseListener(final MouseListener l)1823   public void removeMouseListener(final MouseListener l) {
1824     if (this.component3D != null) {
1825       this.component3D.removeMouseListener(l);
1826     }
1827     super.removeMouseListener(l);
1828   }
1829 
1830   /**
1831    * Returns the closest {@link Selectable} object at component coordinates (x, y),
1832    * or <code>null</code> if not found.
1833    */
getClosestItemAt(int x, int y)1834   public Selectable getClosestItemAt(int x, int y) {
1835     if (this.component3D != null) {
1836       Canvas3D canvas;
1837       if (this.component3D instanceof Canvas3D) {
1838         canvas = (Canvas3D)this.component3D;
1839       } else {
1840         try {
1841           // Call JCanvas3D#getOffscreenCanvas3D by reflection to be able to run under Java 3D 1.3
1842           canvas = (Canvas3D)Class.forName("com.sun.j3d.exp.swing.JCanvas3D").getMethod("getOffscreenCanvas3D").invoke(this.component3D);
1843         } catch (Exception ex) {
1844           UnsupportedOperationException ex2 = new UnsupportedOperationException();
1845           ex2.initCause(ex);
1846           throw ex2;
1847         }
1848       }
1849       PickCanvas pickCanvas = new PickCanvas(canvas, this.onscreenUniverse.getLocale());
1850       pickCanvas.setMode(PickCanvas.GEOMETRY);
1851 
1852       if (OperatingSystem.isJavaVersionGreaterOrEqual("1.9")) {
1853         try {
1854           // Dirty hack that scales mouse coordinates with xcale and yscale private fields of Canvas3D
1855           Field xscaleField = Canvas3D.class.getDeclaredField("xscale");
1856           xscaleField.setAccessible(true);
1857           double xscale = (Double)(xscaleField.get(this.component3D));
1858           Field yscaleField = Canvas3D.class.getDeclaredField("yscale");
1859           yscaleField.setAccessible(true);
1860           double yscale = (Double)(yscaleField.get(this.component3D));
1861           x = (int)(x * xscale);
1862           y = (int)(y * yscale);
1863         } catch (Exception ex) {
1864         }
1865       }
1866 
1867       Point canvasPoint = SwingUtilities.convertPoint(this, x, y, this.component3D);
1868       pickCanvas.setShapeLocation(canvasPoint.x, canvasPoint.y);
1869       PickResult result = pickCanvas.pickClosest();
1870       if (result != null) {
1871         Node pickedNode = result.getNode(PickResult.SHAPE3D);
1872         while (!this.homeObjects.containsValue(pickedNode)
1873                && pickedNode.getParent() != null) {
1874           pickedNode = pickedNode.getParent();
1875         }
1876         if (pickedNode != null) {
1877           for (Map.Entry<Selectable, Object3DBranch> entry : this.homeObjects.entrySet()) {
1878             if (entry.getValue() == pickedNode) {
1879               return entry.getKey();
1880             }
1881           }
1882         }
1883       }
1884     }
1885     return null;
1886   }
1887 
1888   /**
1889    * Returns a new scene tree root.
1890    */
createSceneTree(boolean displayShadowOnFloor, boolean listenToHomeUpdates, boolean waitForLoading)1891   private BranchGroup createSceneTree(boolean displayShadowOnFloor,
1892                                       boolean listenToHomeUpdates,
1893                                       boolean waitForLoading) {
1894     BranchGroup root = new BranchGroup();
1895     // Build scene tree
1896     root.addChild(createHomeTree(displayShadowOnFloor, listenToHomeUpdates, waitForLoading));
1897     Node backgroundNode = createBackgroundNode(listenToHomeUpdates, waitForLoading);
1898     root.addChild(backgroundNode);
1899     Node groundNode = createGroundNode(-0.5E7f, -0.5E7f, 1E7f, 1E7f, listenToHomeUpdates, waitForLoading);
1900     root.addChild(groundNode);
1901 
1902     this.sceneLights = createLights(groundNode, listenToHomeUpdates);
1903     for (Light light : this.sceneLights) {
1904       root.addChild(light);
1905     }
1906 
1907     return root;
1908   }
1909 
1910   /**
1911    * Returns a new background node.
1912    */
createBackgroundNode(boolean listenToHomeUpdates, final boolean waitForLoading)1913   private Node createBackgroundNode(boolean listenToHomeUpdates, final boolean waitForLoading) {
1914     final Appearance skyBackgroundAppearance = new Appearance();
1915     ColoringAttributes skyBackgroundColoringAttributes = new ColoringAttributes();
1916     skyBackgroundAppearance.setColoringAttributes(skyBackgroundColoringAttributes);
1917     TextureAttributes skyBackgroundTextureAttributes = new TextureAttributes();
1918     skyBackgroundAppearance.setTextureAttributes(skyBackgroundTextureAttributes);
1919     // Allow sky color and texture to change
1920     skyBackgroundAppearance.setCapability(Appearance.ALLOW_TEXTURE_WRITE);
1921     skyBackgroundAppearance.setCapability(Appearance.ALLOW_COLORING_ATTRIBUTES_READ);
1922     skyBackgroundColoringAttributes.setCapability(ColoringAttributes.ALLOW_COLOR_WRITE);
1923     skyBackgroundAppearance.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_READ);
1924     skyBackgroundTextureAttributes.setCapability(TextureAttributes.ALLOW_TRANSFORM_WRITE);
1925 
1926     Geometry topHalfSphereGeometry = createHalfSphereGeometry(true);
1927     final Shape3D topHalfSphere = new Shape3D(topHalfSphereGeometry, skyBackgroundAppearance);
1928     BranchGroup backgroundBranch = new BranchGroup();
1929     backgroundBranch.addChild(topHalfSphere);
1930 
1931     final Appearance bottomAppearance = new Appearance();
1932     final RenderingAttributes bottomRenderingAttributes = new RenderingAttributes();
1933     bottomRenderingAttributes.setVisible(false);
1934     bottomAppearance.setRenderingAttributes(bottomRenderingAttributes);
1935     bottomRenderingAttributes.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE);
1936     Shape3D bottomHalfSphere = new Shape3D(createHalfSphereGeometry(false), bottomAppearance);
1937     backgroundBranch.addChild(bottomHalfSphere);
1938 
1939     // Add two planes at ground level to complete landscape at the horizon when camera is above horizon
1940     // (one at y = -0.01 to fill the horizon and a lower one to fill the lower part of the scene)
1941     final Appearance groundBackgroundAppearance = new Appearance();
1942     TextureAttributes groundBackgroundTextureAttributes = new TextureAttributes();
1943     groundBackgroundTextureAttributes.setTextureMode(TextureAttributes.MODULATE);
1944     groundBackgroundAppearance.setTextureAttributes(groundBackgroundTextureAttributes);
1945     groundBackgroundAppearance.setTexCoordGeneration(
1946         new TexCoordGeneration(TexCoordGeneration.OBJECT_LINEAR, TexCoordGeneration.TEXTURE_COORDINATE_2,
1947             new Vector4f(1E5f, 0, 0, 0), new Vector4f(0, 0, 1E5f, 0)));
1948     final RenderingAttributes groundRenderingAttributes = new RenderingAttributes();
1949     groundBackgroundAppearance.setRenderingAttributes(groundRenderingAttributes);
1950     // Allow ground color and texture to change
1951     groundBackgroundAppearance.setCapability(Appearance.ALLOW_TEXTURE_WRITE);
1952     groundBackgroundAppearance.setCapability(Appearance.ALLOW_MATERIAL_WRITE);
1953     groundRenderingAttributes.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE);
1954 
1955     GeometryInfo geometryInfo = new GeometryInfo (GeometryInfo.QUAD_ARRAY);
1956     geometryInfo.setCoordinates(new Point3f [] {
1957           new Point3f(-1f, -0.01f, -1f),
1958           new Point3f(-1f, -0.01f, 1f),
1959           new Point3f(1f, -0.01f, 1f),
1960           new Point3f(1f, -0.01f, -1f),
1961           new Point3f(-1f, -0.1f, -1f),
1962           new Point3f(-1f, -0.1f, 1f),
1963           new Point3f(1f, -0.1f, 1f),
1964           new Point3f(1f, -0.1f, -1f)});
1965     geometryInfo.setCoordinateIndices(new int [] {0, 1, 2, 3, 4, 5, 6, 7});
1966     geometryInfo.setNormals(new Vector3f [] {new Vector3f(0, 1, 0)});
1967     geometryInfo.setNormalIndices(new int [] {0, 0, 0, 0, 0, 0, 0, 0});
1968     Shape3D groundBackground = new Shape3D(geometryInfo.getIndexedGeometryArray(), groundBackgroundAppearance);
1969     backgroundBranch.addChild(groundBackground);
1970 
1971     // Add its own lights to background to ensure they have an effect
1972     for (Light light : createBackgroundLights(listenToHomeUpdates)) {
1973       backgroundBranch.addChild(light);
1974     }
1975 
1976     final Background background = new Background(backgroundBranch);
1977     updateBackgroundColorAndTexture(skyBackgroundAppearance, groundBackgroundAppearance, this.home, waitForLoading);
1978     background.setApplicationBounds(new BoundingBox(
1979         new Point3d(-1E7, -1E7, -1E7),
1980         new Point3d(1E7, 1E7, 1E7)));
1981 
1982     if (listenToHomeUpdates) {
1983       // Add a listener on sky color and texture properties change
1984       this.backgroundChangeListener = new PropertyChangeListener() {
1985           public void propertyChange(PropertyChangeEvent ev) {
1986             updateBackgroundColorAndTexture(skyBackgroundAppearance, groundBackgroundAppearance, home, waitForLoading);
1987           }
1988         };
1989       this.home.getEnvironment().addPropertyChangeListener(
1990           HomeEnvironment.Property.SKY_COLOR, this.backgroundChangeListener);
1991       this.home.getEnvironment().addPropertyChangeListener(
1992           HomeEnvironment.Property.SKY_TEXTURE, this.backgroundChangeListener);
1993       this.home.getEnvironment().addPropertyChangeListener(
1994           HomeEnvironment.Property.GROUND_COLOR, this.backgroundChangeListener);
1995       this.home.getEnvironment().addPropertyChangeListener(
1996           HomeEnvironment.Property.GROUND_TEXTURE, this.backgroundChangeListener);
1997       // Make groundBackground invisible and bottom half sphere visible if camera is below the ground
1998       this.elevationChangeListener = new PropertyChangeListener() {
1999           public void propertyChange(PropertyChangeEvent ev) {
2000             if (ev.getSource() == home) {
2001               // Move listener to the new camera
2002               ((Camera)ev.getOldValue()).removePropertyChangeListener(this);
2003               home.getCamera().addPropertyChangeListener(this);
2004             }
2005             if (ev.getSource() == home
2006                 || Camera.Property.Z.name().equals(ev.getPropertyName())) {
2007               groundRenderingAttributes.setVisible(home.getCamera().getZ() >= 0);
2008               bottomRenderingAttributes.setVisible(home.getCamera().getZ() < 0);
2009             }
2010           }
2011         };
2012       this.home.getCamera().addPropertyChangeListener(this.elevationChangeListener);
2013       this.home.addPropertyChangeListener(Home.Property.CAMERA, this.elevationChangeListener);
2014     }
2015     return background;
2016   }
2017 
2018   /**
2019    * Returns a half sphere oriented inward and with texture ordinates
2020    * that spread along an hemisphere.
2021    */
createHalfSphereGeometry(boolean top)2022   private Geometry createHalfSphereGeometry(boolean top) {
2023     final int divisionCount = 48;
2024     Point3f [] coords = new Point3f [divisionCount * divisionCount];
2025     TexCoord2f [] textureCoords = top ? new TexCoord2f [divisionCount * divisionCount] : null;
2026     Color3f [] colors = top ? null : new Color3f [divisionCount * divisionCount];
2027     for (int i = 0, k = 0; i < divisionCount; i++) {
2028       double alpha = i * 2 * Math.PI / divisionCount;
2029       float cosAlpha = (float)Math.cos(alpha);
2030       float sinAlpha = (float)Math.sin(alpha);
2031       double nextAlpha = (i  + 1) * 2 * Math.PI / divisionCount;
2032       float cosNextAlpha = (float)Math.cos(nextAlpha);
2033       float sinNextAlpha = (float)Math.sin(nextAlpha);
2034       for (int j = 0, max = divisionCount / 4; j < max; j++) {
2035         double beta = 2 * j * Math.PI / divisionCount;
2036         float cosBeta = (float)Math.cos(beta);
2037         float sinBeta = (float)Math.sin(beta);
2038         // Correct the bottom of the hemisphere to avoid seeing a bottom hemisphere at the horizon
2039         float y = j != 0 ? (top ? sinBeta : -sinBeta) : -0.01f;
2040         double nextBeta = 2 * (j + 1) * Math.PI / divisionCount;
2041         if (!top) {
2042           nextBeta = -nextBeta;
2043         }
2044         float cosNextBeta = (float)Math.cos(nextBeta);
2045         float sinNextBeta = (float)Math.sin(nextBeta);
2046         if (top) {
2047           coords [k] = new Point3f(cosAlpha * cosBeta, y, sinAlpha * cosBeta);
2048           textureCoords [k++] = new TexCoord2f((float)i / divisionCount, (float)j / max);
2049 
2050           coords [k] = new Point3f(cosNextAlpha * cosBeta, y, sinNextAlpha * cosBeta);
2051           textureCoords [k++] = new TexCoord2f((float)(i + 1) / divisionCount, (float)j / max);
2052 
2053           coords [k] = new Point3f(cosNextAlpha * cosNextBeta, sinNextBeta, sinNextAlpha * cosNextBeta);
2054           textureCoords [k++] = new TexCoord2f((float)(i + 1) / divisionCount, (float)(j + 1) / max);
2055 
2056           coords [k] = new Point3f(cosAlpha * cosNextBeta, sinNextBeta, sinAlpha * cosNextBeta);
2057           textureCoords [k++] = new TexCoord2f((float)i / divisionCount, (float)(j + 1) / max);
2058         } else {
2059           coords [k] = new Point3f(cosAlpha * cosBeta, y, sinAlpha * cosBeta);
2060           float color1 = .9f + y * .5f;
2061           colors [k++] = new Color3f(color1, color1, color1);
2062 
2063           coords [k] = new Point3f(cosAlpha * cosNextBeta, sinNextBeta, sinAlpha * cosNextBeta);
2064           float color2 = .9f + sinNextBeta * .5f;
2065           colors [k++] = new Color3f(color2, color2, color2);
2066 
2067           coords [k] = new Point3f(cosNextAlpha * cosNextBeta, sinNextBeta, sinNextAlpha * cosNextBeta);
2068           colors [k++] = new Color3f(color2, color2, color2);
2069 
2070           coords [k] = new Point3f(cosNextAlpha * cosBeta, y, sinNextAlpha * cosBeta);
2071           colors [k++] = new Color3f(color1, color1, color1);
2072         }
2073       }
2074     }
2075 
2076     GeometryInfo geometryInfo = new GeometryInfo(GeometryInfo.QUAD_ARRAY);
2077     geometryInfo.setCoordinates(coords);
2078     if (textureCoords != null) {
2079       geometryInfo.setTextureCoordinateParams(1, 2);
2080       geometryInfo.setTextureCoordinates(0, textureCoords);
2081     }
2082     if (colors != null) {
2083       geometryInfo.setColors(colors);
2084     }
2085     geometryInfo.indexify();
2086     geometryInfo.compact();
2087     Geometry halfSphereGeometry = geometryInfo.getIndexedGeometryArray();
2088     return halfSphereGeometry;
2089   }
2090 
2091   /**
2092    * Updates <code>backgroundAppearance</code> color and texture from <code>home</code> sky color and texture.
2093    */
updateBackgroundColorAndTexture(final Appearance skyBackgroundAppearance, final Appearance groundBackgroundAppearance, Home home, boolean waitForLoading)2094   private void updateBackgroundColorAndTexture(final Appearance skyBackgroundAppearance,
2095                                                final Appearance groundBackgroundAppearance,
2096                                                Home home,
2097                                                boolean waitForLoading) {
2098     Color3f skyColor = new Color3f(new Color(home.getEnvironment().getSkyColor()));
2099     skyBackgroundAppearance.getColoringAttributes().setColor(skyColor);
2100     HomeTexture skyTexture = home.getEnvironment().getSkyTexture();
2101     if (skyTexture != null) {
2102       final Transform3D transform = new Transform3D();
2103       transform.setTranslation(new Vector3f(-skyTexture.getXOffset(), 0, 0));
2104       TextureManager textureManager = TextureManager.getInstance();
2105       if (waitForLoading) {
2106         // Don't share the background texture otherwise if might not be rendered correctly
2107         skyBackgroundAppearance.setTexture(textureManager.loadTexture(skyTexture.getImage()));
2108         skyBackgroundAppearance.getTextureAttributes().setTextureTransform(transform);
2109       } else {
2110         textureManager.loadTexture(skyTexture.getImage(), waitForLoading,
2111             new TextureManager.TextureObserver() {
2112                 public void textureUpdated(Texture texture) {
2113                   // Use a copy of the texture in case it's used in an other universe
2114                   skyBackgroundAppearance.setTexture((Texture)texture.cloneNodeComponent(false));
2115                   skyBackgroundAppearance.getTextureAttributes().setTextureTransform(transform);
2116                 }
2117               });
2118       }
2119     } else {
2120       skyBackgroundAppearance.setTexture(null);
2121     }
2122 
2123     HomeTexture groundTexture = home.getEnvironment().getGroundTexture();
2124     if (groundTexture != null) {
2125       groundBackgroundAppearance.setMaterial(new Material(
2126           new Color3f(1, 1, 1), new Color3f(), new Color3f(1, 1, 1), new Color3f(0, 0, 0), 1));
2127       TextureManager textureManager = TextureManager.getInstance();
2128       if (waitForLoading) {
2129         groundBackgroundAppearance.setTexture(textureManager.loadTexture(groundTexture.getImage()));
2130       } else {
2131         textureManager.loadTexture(groundTexture.getImage(), waitForLoading,
2132             new TextureManager.TextureObserver() {
2133                 public void textureUpdated(Texture texture) {
2134                   // Use a copy of the texture in case it's used in an other universe
2135                   groundBackgroundAppearance.setTexture((Texture)texture.cloneNodeComponent(false));
2136                 }
2137               });
2138       }
2139     } else {
2140       int groundColor = home.getEnvironment().getGroundColor();
2141       Color3f color = new Color3f(((groundColor >>> 16) & 0xFF) / 255.f,
2142                                   ((groundColor >>> 8) & 0xFF) / 255.f,
2143                                    (groundColor & 0xFF) / 255.f);
2144       groundBackgroundAppearance.setMaterial(new Material(color, new Color3f(), color, new Color3f(0, 0, 0), 1));
2145       groundBackgroundAppearance.setTexture(null);
2146     }
2147 
2148     clearPrintedImageCache();
2149   }
2150 
2151   /**
2152    * Returns a new ground node.
2153    */
createGroundNode(final float groundOriginX, final float groundOriginY, final float groundWidth, final float groundDepth, boolean listenToHomeUpdates, boolean waitForLoading)2154   private Node createGroundNode(final float groundOriginX,
2155                                 final float groundOriginY,
2156                                 final float groundWidth,
2157                                 final float groundDepth,
2158                                 boolean listenToHomeUpdates,
2159                                 boolean waitForLoading) {
2160     final Ground3D ground3D = new Ground3D(this.home,
2161         groundOriginX, groundOriginY, groundWidth, groundDepth, waitForLoading);
2162     Transform3D translation = new Transform3D();
2163     translation.setTranslation(new Vector3f(0, -0.2f, 0));
2164     TransformGroup transformGroup = new TransformGroup(translation);
2165     transformGroup.addChild(ground3D);
2166 
2167     if (listenToHomeUpdates) {
2168       // Add a listener on ground color and texture properties change
2169       this.groundChangeListener = new PropertyChangeListener() {
2170           private Runnable updater;
2171           public void propertyChange(PropertyChangeEvent ev) {
2172             if (this.updater == null) {
2173               // Group updates
2174               EventQueue.invokeLater(this.updater = new Runnable () {
2175                 public void run() {
2176                   ground3D.update();
2177                   updater = null;
2178                 }
2179               });
2180             }
2181             clearPrintedImageCache();
2182           }
2183         };
2184       HomeEnvironment homeEnvironment = this.home.getEnvironment();
2185       homeEnvironment.addPropertyChangeListener(
2186           HomeEnvironment.Property.GROUND_COLOR, this.groundChangeListener);
2187       homeEnvironment.addPropertyChangeListener(
2188           HomeEnvironment.Property.GROUND_TEXTURE, this.groundChangeListener);
2189       homeEnvironment.addPropertyChangeListener(
2190           HomeEnvironment.Property.BACKGROUND_IMAGE_VISIBLE_ON_GROUND_3D, this.groundChangeListener);
2191       this.home.addPropertyChangeListener(Home.Property.BACKGROUND_IMAGE, this.groundChangeListener);
2192     }
2193 
2194     return transformGroup;
2195   }
2196 
2197   /**
2198    * Returns the lights used for the background.
2199    */
createBackgroundLights(boolean listenToHomeUpdates)2200   private Light [] createBackgroundLights(boolean listenToHomeUpdates) {
2201     final Light [] lights = {
2202         // Use just one direct light for background because only one horizontal plane is under light
2203         new DirectionalLight(new Color3f(1.435f, 1.435f, 1.435f), new Vector3f(0f, -1f, 0f)),
2204         new AmbientLight(new Color3f(0.2f, 0.2f, 0.2f))};
2205     for (int i = 0; i < lights.length - 1; i++) {
2206       // Allow directional lights color and influencing bounds to change
2207       lights [i].setCapability(DirectionalLight.ALLOW_COLOR_WRITE);
2208       // Store default color in user data
2209       Color3f defaultColor = new Color3f();
2210       lights [i].getColor(defaultColor);
2211       lights [i].setUserData(defaultColor);
2212       updateLightColor(lights [i]);
2213     }
2214 
2215     final Bounds defaultInfluencingBounds = new BoundingSphere(new Point3d(), 2);
2216     for (Light light : lights) {
2217       light.setInfluencingBounds(defaultInfluencingBounds);
2218     }
2219 
2220     if (listenToHomeUpdates) {
2221       // Add a listener on light color property change to home
2222       this.backgroundLightColorListener = new PropertyChangeListener() {
2223           public void propertyChange(PropertyChangeEvent ev) {
2224             updateLightColor(lights [0]);
2225           }
2226         };
2227       this.home.getEnvironment().addPropertyChangeListener(
2228           HomeEnvironment.Property.LIGHT_COLOR, this.backgroundLightColorListener);
2229     }
2230 
2231     return lights;
2232   }
2233 
2234   /**
2235    * Returns the lights of the scene.
2236    */
createLights(final Node groundNode, boolean listenToHomeUpdates)2237   private Light [] createLights(final Node groundNode, boolean listenToHomeUpdates) {
2238     final Light [] lights = {
2239         new DirectionalLight(new Color3f(1, 1, 1), new Vector3f(1.5f, -0.8f, -1)),
2240         new DirectionalLight(new Color3f(1, 1, 1), new Vector3f(-1.5f, -0.8f, -1)),
2241         new DirectionalLight(new Color3f(1, 1, 1), new Vector3f(0, -0.8f, 1)),
2242         new DirectionalLight(new Color3f(0.7f, 0.7f, 0.7f), new Vector3f(0, 1f, 0)),
2243         new AmbientLight(new Color3f(0.2f, 0.2f, 0.2f))};
2244     for (int i = 0; i < lights.length - 1; i++) {
2245       // Allow directional lights color and influencing bounds to change
2246       lights [i].setCapability(DirectionalLight.ALLOW_COLOR_WRITE);
2247       lights [i].setCapability(DirectionalLight.ALLOW_SCOPE_WRITE);
2248       // Store default color in user data
2249       Color3f defaultColor = new Color3f();
2250       lights [i].getColor(defaultColor);
2251       lights [i].setUserData(defaultColor);
2252       updateLightColor(lights [i]);
2253     }
2254 
2255     final Bounds defaultInfluencingBounds = new BoundingSphere(new Point3d(), 1E7);
2256     for (Light light : lights) {
2257       light.setInfluencingBounds(defaultInfluencingBounds);
2258     }
2259 
2260     if (listenToHomeUpdates) {
2261       // Add a listener on light color property change to home
2262       this.lightColorListener = new PropertyChangeListener() {
2263           public void propertyChange(PropertyChangeEvent ev) {
2264             for (int i = 0; i < lights.length - 1; i++) {
2265               updateLightColor(lights [i]);
2266             }
2267             updateObjects(getHomeObjects(HomeLight.class));
2268           }
2269         };
2270       this.home.getEnvironment().addPropertyChangeListener(
2271           HomeEnvironment.Property.LIGHT_COLOR, this.lightColorListener);
2272 
2273       // Add a listener on subpart size property change to home
2274       this.subpartSizeListener = new PropertyChangeListener() {
2275           public void propertyChange(PropertyChangeEvent ev) {
2276             if (ev != null) {
2277               // Update 3D objects if not at initialization
2278               Collection<Selectable> homeItems = new ArrayList<Selectable>(home.getWalls());
2279               homeItems.addAll(home.getRooms());
2280               homeItems.addAll(getHomeObjects(HomeLight.class));
2281               updateObjects(homeItems);
2282               clearPrintedImageCache();
2283             }
2284 
2285             // Update default lights scope
2286             List<Group> scope = null;
2287             if (home.getEnvironment().getSubpartSizeUnderLight() > 0) {
2288               Area lightScopeOutsideWallsArea = getLightScopeOutsideWallsArea();
2289               scope = new ArrayList<Group>();
2290               for (Wall wall : home.getWalls()) {
2291                 Object3DBranch wall3D = homeObjects.get(wall);
2292                 if (wall3D instanceof Wall3D) {
2293                   // Add left and/or right side of the wall to scope
2294                   float [][] points = wall.getPoints();
2295                   if (!lightScopeOutsideWallsArea.contains(points [0][0], points [0][1])) {
2296                     scope.add((Group)wall3D.getChild(1));
2297                   }
2298                   if (!lightScopeOutsideWallsArea.contains(points [points.length - 1][0], points [points.length - 1][1])) {
2299                     scope.add((Group)wall3D.getChild(4));
2300                   }
2301                 }
2302                 // Add wall top and bottom groups to scope
2303                 scope.add((Group)wall3D.getChild(0));
2304                 scope.add((Group)wall3D.getChild(2));
2305                 scope.add((Group)wall3D.getChild(3));
2306                 scope.add((Group)wall3D.getChild(5));
2307               }
2308               List<Selectable> otherItems = new ArrayList<Selectable>(home.getRooms());
2309               otherItems.addAll(getHomeObjects(HomePieceOfFurniture.class));
2310               for (Selectable item : otherItems) {
2311                 // Add item to scope if one of its points don't belong to lightScopeWallsArea
2312                 for (float [] point : item.getPoints()) {
2313                   if (!lightScopeOutsideWallsArea.contains(point [0], point [1])) {
2314                     Group object3D = homeObjects.get(item);
2315                     if (object3D instanceof HomePieceOfFurniture3D) {
2316                       // Add the direct parent of the shape that will be added once loaded
2317                       // otherwise scope won't be updated automatically
2318                       object3D = (Group)object3D.getChild(0);
2319                     }
2320                     scope.add(object3D);
2321                     break;
2322                   }
2323                 }
2324               }
2325             } else {
2326               lightScopeOutsideWallsAreaCache = null;
2327             }
2328 
2329             for (Light light : lights) {
2330               if (light instanceof DirectionalLight) {
2331                 light.removeAllScopes();
2332                 if (scope != null) {
2333                   light.addScope((Group)groundNode);
2334                   for (Group group : scope) {
2335                     light.addScope(group);
2336                   }
2337                 }
2338               }
2339             }
2340           }
2341         };
2342 
2343       this.home.getEnvironment().addPropertyChangeListener(
2344           HomeEnvironment.Property.SUBPART_SIZE_UNDER_LIGHT, this.subpartSizeListener);
2345       this.subpartSizeListener.propertyChange(null);
2346     }
2347 
2348     return lights;
2349   }
2350 
2351   /**
2352    * Returns the home objects displayed by this component of the given class.
2353    */
getHomeObjects(Class<T> objectClass)2354   private <T> List<T> getHomeObjects(Class<T> objectClass) {
2355     return Home.getSubList(new ArrayList<Selectable>(homeObjects.keySet()), objectClass);
2356   }
2357 
2358   /**
2359    * Updates<code>light</code> color from <code>home</code> light color.
2360    */
updateLightColor(Light light)2361   private void updateLightColor(Light light) {
2362     Color3f defaultColor = (Color3f)light.getUserData();
2363     int lightColor = this.home.getEnvironment().getLightColor();
2364     light.setColor(new Color3f(((lightColor >>> 16) & 0xFF) / 255f * defaultColor.x,
2365                                 ((lightColor >>> 8) & 0xFF) / 255f * defaultColor.y,
2366                                         (lightColor & 0xFF) / 255f * defaultColor.z));
2367     clearPrintedImageCache();
2368   }
2369 
2370   /**
2371    * Returns walls area used for light scope outside.
2372    */
getLightScopeOutsideWallsArea()2373   private Area getLightScopeOutsideWallsArea() {
2374     if (this.lightScopeOutsideWallsAreaCache == null) {
2375       // Compute a smaller area surrounding all walls at all levels
2376       Area wallsPath = new Area();
2377       for (Wall wall : this.home.getWalls()) {
2378         Wall thinnerWall = wall.clone();
2379         thinnerWall.setThickness(Math.max(thinnerWall.getThickness() - 0.1f, 0.08f));
2380         wallsPath.add(new Area(getShape(thinnerWall.getPoints())));
2381       }
2382       Area lightScopeOutsideWallsArea = new Area();
2383       List<float []> points = new ArrayList<float[]>();
2384       for (PathIterator it = wallsPath.getPathIterator(null, 1); !it.isDone(); it.next()) {
2385         float [] point = new float[2];
2386         switch (it.currentSegment(point)) {
2387           case PathIterator.SEG_MOVETO :
2388           case PathIterator.SEG_LINETO :
2389             points.add(point);
2390             break;
2391           case PathIterator.SEG_CLOSE :
2392             if (points.size() > 2) {
2393               float [][] pointsArray = points.toArray(new float [points.size()][]);
2394               if (new Room(pointsArray).isClockwise()) {
2395                 lightScopeOutsideWallsArea.add(new Area(getShape(pointsArray)));
2396               }
2397             }
2398             points.clear();
2399             break;
2400         }
2401       }
2402       this.lightScopeOutsideWallsAreaCache = lightScopeOutsideWallsArea;
2403     }
2404     return this.lightScopeOutsideWallsAreaCache;
2405   }
2406 
2407   /**
2408    * Returns a <code>home</code> new tree node, with branches for each wall
2409    * and piece of furniture of <code>home</code>.
2410    */
createHomeTree(boolean displayShadowOnFloor, boolean listenToHomeUpdates, boolean waitForLoading)2411   private Node createHomeTree(boolean displayShadowOnFloor,
2412                               boolean listenToHomeUpdates,
2413                               boolean waitForLoading) {
2414     Group homeRoot = createHomeRoot();
2415     // Add walls, pieces, rooms, polylines and labels already available
2416     for (Label label : this.home.getLabels()) {
2417       addObject(homeRoot, label, listenToHomeUpdates, waitForLoading);
2418     }
2419     for (Polyline polyline : this.home.getPolylines()) {
2420       addObject(homeRoot, polyline, listenToHomeUpdates, waitForLoading);
2421     }
2422     for (Room room : this.home.getRooms()) {
2423       addObject(homeRoot, room, listenToHomeUpdates, waitForLoading);
2424     }
2425     for (Wall wall : this.home.getWalls()) {
2426       addObject(homeRoot, wall, listenToHomeUpdates, waitForLoading);
2427     }
2428     Map<HomePieceOfFurniture, Node> pieces3D = new HashMap<HomePieceOfFurniture, Node>();
2429     for (HomePieceOfFurniture piece : this.home.getFurniture()) {
2430       if (piece instanceof HomeFurnitureGroup) {
2431         for (HomePieceOfFurniture childPiece : ((HomeFurnitureGroup)piece).getAllFurniture()) {
2432           if (!(childPiece instanceof HomeFurnitureGroup)) {
2433             pieces3D.put(childPiece, addObject(homeRoot, childPiece, listenToHomeUpdates, waitForLoading));
2434           }
2435         }
2436       } else {
2437         pieces3D.put(piece, addObject(homeRoot, piece, listenToHomeUpdates, waitForLoading));
2438       }
2439     }
2440 
2441     if (displayShadowOnFloor) {
2442       addShadowOnFloor(homeRoot, pieces3D);
2443     }
2444 
2445     if (listenToHomeUpdates) {
2446       // Add level, wall, furniture, room listeners to home for further update
2447       addLevelListener(homeRoot);
2448       addWallListener(homeRoot);
2449       addFurnitureListener(homeRoot);
2450       addRoomListener(homeRoot);
2451       addPolylineListener(homeRoot);
2452       addLabelListener(homeRoot);
2453       // Add environment listeners
2454       addEnvironmentListeners();
2455       // Should update shadow on floor too but in the facts
2456       // User Interface doesn't propose to modify the furniture of a home
2457       // that displays shadow on floor yet
2458     }
2459     return homeRoot;
2460   }
2461 
2462   /**
2463    * Returns a new group at home subtree root.
2464    */
createHomeRoot()2465   private Group createHomeRoot() {
2466     Group homeGroup = new Group();
2467     //  Allow group to have new children
2468     homeGroup.setCapability(Group.ALLOW_CHILDREN_WRITE);
2469     homeGroup.setCapability(Group.ALLOW_CHILDREN_EXTEND);
2470     return homeGroup;
2471   }
2472 
2473   /**
2474    * Adds a level listener to home levels that updates the children of the given
2475    * <code>group</code>, each time a level is added, updated or deleted.
2476    */
addLevelListener(final Group group)2477   private void addLevelListener(final Group group) {
2478     this.levelChangeListener = new PropertyChangeListener() {
2479         public void propertyChange(PropertyChangeEvent ev) {
2480           if (Level.Property.VISIBLE.name().equals(ev.getPropertyName())
2481               || Level.Property.VIEWABLE.name().equals(ev.getPropertyName())) {
2482             Set<Selectable> objects = homeObjects.keySet();
2483             ArrayList<Selectable> updatedItems = new ArrayList<Selectable>(objects.size());
2484             for (Selectable item : objects) {
2485               if (item instanceof Room // 3D rooms depend on rooms at other levels
2486                   || !(item instanceof Elevatable)
2487                   || ((Elevatable)item).isAtLevel((Level)ev.getSource())) {
2488                 updatedItems.add(item);
2489               }
2490             }
2491             updateObjects(updatedItems);
2492             groundChangeListener.propertyChange(null);
2493           } else if (Level.Property.ELEVATION.name().equals(ev.getPropertyName())) {
2494             updateObjects(homeObjects.keySet());
2495             groundChangeListener.propertyChange(null);
2496           } else if (Level.Property.BACKGROUND_IMAGE.name().equals(ev.getPropertyName())) {
2497             groundChangeListener.propertyChange(null);
2498           } else if (Level.Property.FLOOR_THICKNESS.name().equals(ev.getPropertyName())) {
2499             updateObjects(home.getWalls());
2500             updateObjects(home.getRooms());
2501           } else if (Level.Property.HEIGHT.name().equals(ev.getPropertyName())) {
2502             updateObjects(home.getRooms());
2503           }
2504         }
2505       };
2506     for (Level level : this.home.getLevels()) {
2507       level.addPropertyChangeListener(this.levelChangeListener);
2508     }
2509     this.levelListener = new CollectionListener<Level>() {
2510         public void collectionChanged(CollectionEvent<Level> ev) {
2511           Level level = ev.getItem();
2512           switch (ev.getType()) {
2513             case ADD :
2514               level.addPropertyChangeListener(levelChangeListener);
2515               break;
2516             case DELETE :
2517               level.removePropertyChangeListener(levelChangeListener);
2518               break;
2519           }
2520           updateObjects(home.getRooms());
2521         }
2522       };
2523     this.home.addLevelsListener(this.levelListener);
2524   }
2525 
2526   /**
2527    * Adds a wall listener to home walls that updates the children of the given
2528    * <code>group</code>, each time a wall is added, updated or deleted.
2529    */
addWallListener(final Group group)2530   private void addWallListener(final Group group) {
2531     this.wallChangeListener = new PropertyChangeListener() {
2532         public void propertyChange(PropertyChangeEvent ev) {
2533           String propertyName = ev.getPropertyName();
2534           if (!Wall.Property.PATTERN.name().equals(propertyName)) {
2535             Wall updatedWall = (Wall)ev.getSource();
2536             updateWall(updatedWall);
2537             List<Level> levels = home.getLevels();
2538             if (updatedWall.getLevel() == null
2539                 || updatedWall.isAtLevel(levels.get(levels.size() - 1))) {
2540               // Update rooms which ceiling height may need an update at last level
2541               updateObjects(home.getRooms());
2542             }
2543             if (updatedWall.getLevel() != null && updatedWall.getLevel().getElevation() < 0) {
2544               groundChangeListener.propertyChange(null);
2545             }
2546             if (home.getEnvironment().getSubpartSizeUnderLight() > 0) {
2547               if (Wall.Property.X_START.name().equals(propertyName)
2548                   || Wall.Property.Y_START.name().equals(propertyName)
2549                   || Wall.Property.X_END.name().equals(propertyName)
2550                   || Wall.Property.Y_END.name().equals(propertyName)
2551                   || Wall.Property.ARC_EXTENT.name().equals(propertyName)
2552                   || Wall.Property.THICKNESS.name().equals(propertyName)) {
2553                 lightScopeOutsideWallsAreaCache = null;
2554                 updateObjectsLightScope(null);
2555               }
2556             }
2557           }
2558         }
2559       };
2560     for (Wall wall : this.home.getWalls()) {
2561       wall.addPropertyChangeListener(this.wallChangeListener);
2562     }
2563     this.wallListener = new CollectionListener<Wall>() {
2564         public void collectionChanged(CollectionEvent<Wall> ev) {
2565           Wall wall = ev.getItem();
2566           switch (ev.getType()) {
2567             case ADD :
2568               addObject(group, wall, true, false);
2569               wall.addPropertyChangeListener(wallChangeListener);
2570               break;
2571             case DELETE :
2572               deleteObject(wall);
2573               wall.removePropertyChangeListener(wallChangeListener);
2574               break;
2575           }
2576           lightScopeOutsideWallsAreaCache = null;
2577           updateObjects(home.getRooms());
2578           groundChangeListener.propertyChange(null);
2579           updateObjectsLightScope(null);
2580         }
2581       };
2582     this.home.addWallsListener(this.wallListener);
2583   }
2584 
2585   /**
2586    * Adds a furniture listener to home that updates the children of the given <code>group</code>,
2587    * each time a piece of furniture is added, updated or deleted.
2588    */
addFurnitureListener(final Group group)2589   private void addFurnitureListener(final Group group) {
2590     this.furnitureChangeListener = new PropertyChangeListener() {
2591         public void propertyChange(PropertyChangeEvent ev) {
2592           HomePieceOfFurniture updatedPiece = (HomePieceOfFurniture)ev.getSource();
2593           String propertyName = ev.getPropertyName();
2594           if (HomePieceOfFurniture.Property.X.name().equals(propertyName)
2595               || HomePieceOfFurniture.Property.Y.name().equals(propertyName)
2596               || HomePieceOfFurniture.Property.ANGLE.name().equals(propertyName)
2597               || HomePieceOfFurniture.Property.ROLL.name().equals(propertyName)
2598               || HomePieceOfFurniture.Property.PITCH.name().equals(propertyName)
2599               || HomePieceOfFurniture.Property.WIDTH.name().equals(propertyName)
2600               || HomePieceOfFurniture.Property.DEPTH.name().equals(propertyName)) {
2601             updatePieceOfFurnitureGeometry(updatedPiece, propertyName, (Float)ev.getOldValue());
2602             updateObjectsLightScope(Arrays.asList(new HomePieceOfFurniture [] {updatedPiece}));
2603           } else if (HomePieceOfFurniture.Property.HEIGHT.name().equals(propertyName)
2604               || HomePieceOfFurniture.Property.ELEVATION.name().equals(propertyName)
2605               || HomePieceOfFurniture.Property.MODEL.name().equals(propertyName)
2606               || HomePieceOfFurniture.Property.MODEL_ROTATION.name().equals(propertyName)
2607               || HomePieceOfFurniture.Property.MODEL_MIRRORED.name().equals(propertyName)
2608               || HomePieceOfFurniture.Property.BACK_FACE_SHOWN.name().equals(propertyName)
2609               || HomePieceOfFurniture.Property.MODEL_TRANSFORMATIONS.name().equals(propertyName)
2610               || HomePieceOfFurniture.Property.STAIRCASE_CUT_OUT_SHAPE.name().equals(propertyName)
2611               || HomePieceOfFurniture.Property.VISIBLE.name().equals(propertyName)
2612               || HomePieceOfFurniture.Property.LEVEL.name().equals(propertyName)) {
2613             updatePieceOfFurnitureGeometry(updatedPiece, null, null);
2614           } else if (HomeDoorOrWindow.Property.CUT_OUT_SHAPE.name().equals(propertyName)
2615               || HomeDoorOrWindow.Property.WALL_CUT_OUT_ON_BOTH_SIDES.name().equals(propertyName)
2616               || HomeDoorOrWindow.Property.WALL_WIDTH.name().equals(propertyName)
2617               || HomeDoorOrWindow.Property.WALL_LEFT.name().equals(propertyName)
2618               || HomeDoorOrWindow.Property.WALL_HEIGHT.name().equals(propertyName)
2619               || HomeDoorOrWindow.Property.WALL_TOP.name().equals(propertyName)) {
2620             if (containsDoorsAndWindows(updatedPiece)) {
2621               updateIntersectingWalls(updatedPiece);
2622             }
2623           } else if (HomePieceOfFurniture.Property.COLOR.name().equals(propertyName)
2624               || HomePieceOfFurniture.Property.TEXTURE.name().equals(propertyName)
2625               || HomePieceOfFurniture.Property.MODEL_MATERIALS.name().equals(propertyName)
2626               || HomePieceOfFurniture.Property.SHININESS.name().equals(propertyName)
2627               || (HomeLight.Property.POWER.name().equals(propertyName)
2628                   && home.getEnvironment().getSubpartSizeUnderLight() > 0)) {
2629             updateObjects(Arrays.asList(new HomePieceOfFurniture [] {updatedPiece}));
2630           }
2631         }
2632 
2633         private void updatePieceOfFurnitureGeometry(HomePieceOfFurniture piece, String propertyName, Float oldValue) {
2634           updateObjects(Arrays.asList(new HomePieceOfFurniture [] {piece}));
2635           if (containsDoorsAndWindows(piece)) {
2636             if (oldValue != null) {
2637               HomePieceOfFurniture oldPiece = piece.clone();
2638               // Reset the modified property to its old value
2639               if (HomePieceOfFurniture.Property.X.name().equals(propertyName)) {
2640                 oldPiece.setX(oldValue);
2641               } else if (HomePieceOfFurniture.Property.Y.name().equals(propertyName)) {
2642                 oldPiece.setY(oldValue);
2643               } else if (HomePieceOfFurniture.Property.ANGLE.name().equals(propertyName)) {
2644                 oldPiece.setAngle(oldValue);
2645               } else if (HomePieceOfFurniture.Property.WIDTH.name().equals(propertyName)) {
2646                 oldPiece.setWidth(oldValue);
2647               } else if (HomePieceOfFurniture.Property.DEPTH.name().equals(propertyName)) {
2648                 oldPiece.setDepth(oldValue);
2649               }
2650               // For doors and windows, propertyName can't be equal to ROLL or PITCH
2651 
2652               // Update walls which intersect the piece with its old property value and the one with the new value
2653               updateIntersectingWalls(oldPiece, piece);
2654             } else {
2655               // Property value change won't influence the walls that intersect the door or window
2656               updateIntersectingWalls(piece);
2657             }
2658           } else if (containsStaircases(piece)) {
2659             updateObjects(home.getRooms());
2660           }
2661           if (piece.getLevel() != null && piece.getLevel().getElevation() < 0) {
2662             groundChangeListener.propertyChange(null);
2663           }
2664         }
2665       };
2666     for (HomePieceOfFurniture piece : this.home.getFurniture()) {
2667       addPropertyChangeListener(piece, this.furnitureChangeListener);
2668     }
2669     this.furnitureListener = new CollectionListener<HomePieceOfFurniture>() {
2670         public void collectionChanged(CollectionEvent<HomePieceOfFurniture> ev) {
2671           HomePieceOfFurniture piece = (HomePieceOfFurniture)ev.getItem();
2672           switch (ev.getType()) {
2673             case ADD :
2674               addPieceOfFurniture(group, piece, true, false);
2675               addPropertyChangeListener(piece, furnitureChangeListener);
2676               break;
2677             case DELETE :
2678               deletePieceOfFurniture(piece);
2679               removePropertyChangeListener(piece, furnitureChangeListener);
2680               break;
2681           }
2682           // If piece is or contains a door or a window, update walls that intersect with piece
2683           if (containsDoorsAndWindows(piece)) {
2684             updateIntersectingWalls(piece);
2685           } else if (containsStaircases(piece)) {
2686             updateObjects(home.getRooms());
2687           } else {
2688             approximateHomeBoundsCache = null;
2689           }
2690           groundChangeListener.propertyChange(null);
2691           updateObjectsLightScope(Arrays.asList(new HomePieceOfFurniture [] {piece}));
2692         }
2693       };
2694     this.home.addFurnitureListener(this.furnitureListener);
2695   }
2696 
2697   /**
2698    * Adds the given <code>listener</code> to <code>piece</code> and its children.
2699    */
addPropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener)2700   private void addPropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener) {
2701     if (piece instanceof HomeFurnitureGroup) {
2702       for (HomePieceOfFurniture child : ((HomeFurnitureGroup)piece).getFurniture()) {
2703         addPropertyChangeListener(child, listener);
2704       }
2705     } else {
2706       piece.addPropertyChangeListener(listener);
2707     }
2708   }
2709 
2710   /**
2711    * Removes the given <code>listener</code> from <code>piece</code> and its children.
2712    */
removePropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener)2713   private void removePropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener) {
2714     if (piece instanceof HomeFurnitureGroup) {
2715       for (HomePieceOfFurniture child : ((HomeFurnitureGroup)piece).getFurniture()) {
2716         removePropertyChangeListener(child, listener);
2717       }
2718     } else {
2719       piece.removePropertyChangeListener(listener);
2720     }
2721   }
2722 
2723   /**
2724    * Returns <code>true</code> if the given <code>piece</code> is or contains a door or window.
2725    */
containsDoorsAndWindows(HomePieceOfFurniture piece)2726   private boolean containsDoorsAndWindows(HomePieceOfFurniture piece) {
2727     if (piece instanceof HomeFurnitureGroup) {
2728       for (HomePieceOfFurniture groupPiece : ((HomeFurnitureGroup)piece).getFurniture()) {
2729         if (containsDoorsAndWindows(groupPiece)) {
2730           return true;
2731         }
2732       }
2733       return false;
2734     } else {
2735       return piece.isDoorOrWindow();
2736     }
2737   }
2738 
2739   /**
2740    * Returns <code>true</code> if the given <code>piece</code> is or contains a staircase
2741    * with a top cut out shape.
2742    */
containsStaircases(HomePieceOfFurniture piece)2743   private boolean containsStaircases(HomePieceOfFurniture piece) {
2744     if (piece instanceof HomeFurnitureGroup) {
2745       for (HomePieceOfFurniture groupPiece : ((HomeFurnitureGroup)piece).getFurniture()) {
2746         if (containsStaircases(groupPiece)) {
2747           return true;
2748         }
2749       }
2750       return false;
2751     } else {
2752       return piece.getStaircaseCutOutShape() != null;
2753     }
2754   }
2755 
2756   /**
2757    * Adds a room listener to home rooms that updates the children of the given
2758    * <code>group</code>, each time a room is added, updated or deleted.
2759    */
addRoomListener(final Group group)2760   private void addRoomListener(final Group group) {
2761     this.roomChangeListener = new PropertyChangeListener() {
2762         public void propertyChange(PropertyChangeEvent ev) {
2763           Room updatedRoom = (Room)ev.getSource();
2764           String propertyName = ev.getPropertyName();
2765           if (Room.Property.FLOOR_COLOR.name().equals(propertyName)
2766               || Room.Property.FLOOR_TEXTURE.name().equals(propertyName)
2767               || Room.Property.FLOOR_SHININESS.name().equals(propertyName)
2768               || Room.Property.CEILING_COLOR.name().equals(propertyName)
2769               || Room.Property.CEILING_TEXTURE.name().equals(propertyName)
2770               || Room.Property.CEILING_SHININESS.name().equals(propertyName)) {
2771             updateObjects(Arrays.asList(new Room [] {updatedRoom}));
2772           } else if (Room.Property.FLOOR_VISIBLE.name().equals(propertyName)
2773               || Room.Property.CEILING_VISIBLE.name().equals(propertyName)
2774               || Room.Property.LEVEL.name().equals(propertyName)) {
2775             updateObjects(home.getRooms());
2776             groundChangeListener.propertyChange(null);
2777           } else if (Room.Property.POINTS.name().equals(propertyName)) {
2778             if (homeObjectsToUpdate != null) {
2779               // Don't try to optimize if more than one room to update
2780               updateObjects(home.getRooms());
2781             } else {
2782               updateObjects(Arrays.asList(new Room [] {updatedRoom}));
2783               updateObjects(getHomeObjects(HomeLight.class));
2784               // Search the rooms that overlap the updated one
2785               Area oldArea = new Area(getShape((float [][])ev.getOldValue()));
2786               Area newArea = new Area(getShape((float [][])ev.getNewValue()));
2787               Level updatedRoomLevel = updatedRoom.getLevel();
2788               for (Room room : home.getRooms()) {
2789                 Level roomLevel = room.getLevel();
2790                 if (room != updatedRoom
2791                     && (roomLevel == null
2792                         || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() + roomLevel.getHeight())) < 1E-5
2793                         || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() - roomLevel.getFloorThickness())) < 1E-5)) {
2794                   Area roomAreaIntersectionWithOldArea = new Area(getShape(room.getPoints()));
2795                   Area roomAreaIntersectionWithNewArea = new Area(roomAreaIntersectionWithOldArea);
2796                   roomAreaIntersectionWithNewArea.intersect(newArea);
2797                   if (!roomAreaIntersectionWithNewArea.isEmpty()) {
2798                     updateObjects(Arrays.asList(new Room [] {room}));
2799                   } else {
2800                     roomAreaIntersectionWithOldArea.intersect(oldArea);
2801                     if (!roomAreaIntersectionWithOldArea.isEmpty()) {
2802                       updateObjects(Arrays.asList(new Room [] {room}));
2803                     }
2804                   }
2805                 }
2806               }
2807             }
2808             groundChangeListener.propertyChange(null);
2809             updateObjectsLightScope(Arrays.asList(new Room [] {updatedRoom}));
2810             updateObjectsLightScope(getHomeObjects(HomeLight.class));
2811           }
2812         }
2813       };
2814     for (Room room : this.home.getRooms()) {
2815       room.addPropertyChangeListener(this.roomChangeListener);
2816     }
2817     this.roomListener = new CollectionListener<Room>() {
2818         public void collectionChanged(CollectionEvent<Room> ev) {
2819           Room room = ev.getItem();
2820           switch (ev.getType()) {
2821             case ADD :
2822               // Add room to its group at the index indicated by the event
2823               // to ensure the 3D rooms are drawn in the same order as in the plan
2824               addObject(group, room, ev.getIndex(), true, false);
2825               room.addPropertyChangeListener(roomChangeListener);
2826               break;
2827             case DELETE :
2828               deleteObject(room);
2829               room.removePropertyChangeListener(roomChangeListener);
2830               break;
2831           }
2832           updateObjects(home.getRooms());
2833           groundChangeListener.propertyChange(null);
2834           updateObjectsLightScope(Arrays.asList(new Room [] {room}));
2835           updateObjectsLightScope(getHomeObjects(HomeLight.class));
2836         }
2837       };
2838     this.home.addRoomsListener(this.roomListener);
2839   }
2840 
2841   /**
2842    * Returns the path matching points.
2843    */
getShape(float [][] points)2844   private GeneralPath getShape(float [][] points) {
2845     GeneralPath path = new GeneralPath();
2846     path.moveTo(points [0][0], points [0][1]);
2847     for (int i = 1; i < points.length; i++) {
2848       path.lineTo(points [i][0], points [i][1]);
2849     }
2850     path.closePath();
2851     return path;
2852   }
2853 
2854   /**
2855    * Adds a polyline listener to home polylines that updates the children of the given
2856    * <code>group</code>, each time a polyline is added, updated or deleted.
2857    */
addPolylineListener(final Group group)2858   private void addPolylineListener(final Group group) {
2859     this.polylineChangeListener = new PropertyChangeListener() {
2860         public void propertyChange(PropertyChangeEvent ev) {
2861           Polyline polyline = (Polyline)ev.getSource();
2862           updateObjects(Arrays.asList(new Polyline [] {polyline}));
2863         }
2864       };
2865     for (Polyline polyline : this.home.getPolylines()) {
2866       polyline.addPropertyChangeListener(this.polylineChangeListener);
2867     }
2868     this.polylineListener = new CollectionListener<Polyline>() {
2869         public void collectionChanged(CollectionEvent<Polyline> ev) {
2870           Polyline polyline = ev.getItem();
2871           switch (ev.getType()) {
2872             case ADD :
2873               addObject(group, polyline, true, false);
2874               polyline.addPropertyChangeListener(polylineChangeListener);
2875               break;
2876             case DELETE :
2877               deleteObject(polyline);
2878               polyline.removePropertyChangeListener(polylineChangeListener);
2879               break;
2880           }
2881         }
2882       };
2883     this.home.addPolylinesListener(this.polylineListener);
2884   }
2885 
2886   /**
2887    * Adds a label listener to home labels that updates the children of the given
2888    * <code>group</code>, each time a label is added, updated or deleted.
2889    */
addLabelListener(final Group group)2890   private void addLabelListener(final Group group) {
2891     this.labelChangeListener = new PropertyChangeListener() {
2892         public void propertyChange(PropertyChangeEvent ev) {
2893           Label label = (Label)ev.getSource();
2894           updateObjects(Arrays.asList(new Label [] {label}));
2895         }
2896       };
2897     for (Label label : this.home.getLabels()) {
2898       label.addPropertyChangeListener(this.labelChangeListener);
2899     }
2900     this.labelListener = new CollectionListener<Label>() {
2901         public void collectionChanged(CollectionEvent<Label> ev) {
2902           Label label = ev.getItem();
2903           switch (ev.getType()) {
2904             case ADD :
2905               addObject(group, label, true, false);
2906               label.addPropertyChangeListener(labelChangeListener);
2907               break;
2908             case DELETE :
2909               deleteObject(label);
2910               label.removePropertyChangeListener(labelChangeListener);
2911               break;
2912           }
2913         }
2914       };
2915     this.home.addLabelsListener(this.labelListener);
2916   }
2917 
2918   /**
2919    * Adds a walls alpha change listener and drawing mode change listener to home
2920    * environment that updates the home scene objects appearance.
2921    */
addEnvironmentListeners()2922   private void addEnvironmentListeners() {
2923     this.wallsAlphaListener = new PropertyChangeListener() {
2924         public void propertyChange(PropertyChangeEvent ev) {
2925           updateObjects(home.getWalls());
2926           updateObjects(home.getRooms());
2927         }
2928       };
2929     this.home.getEnvironment().addPropertyChangeListener(
2930         HomeEnvironment.Property.WALLS_ALPHA, this.wallsAlphaListener);
2931     this.drawingModeListener = new PropertyChangeListener() {
2932         public void propertyChange(PropertyChangeEvent ev) {
2933           updateObjects(home.getWalls());
2934           updateObjects(home.getRooms());
2935           updateObjects(getHomeObjects(HomePieceOfFurniture.class));
2936         }
2937       };
2938     this.home.getEnvironment().addPropertyChangeListener(
2939         HomeEnvironment.Property.DRAWING_MODE, this.drawingModeListener);
2940   }
2941 
2942   /**
2943    * Adds to <code>group</code> a branch matching <code>homeObject</code>.
2944    */
addObject(Group group, Selectable homeObject, boolean listenToHomeUpdates, boolean waitForLoading)2945   private Node addObject(Group group, Selectable homeObject, boolean listenToHomeUpdates, boolean waitForLoading) {
2946     return addObject(group, homeObject, -1, listenToHomeUpdates, waitForLoading);
2947   }
2948 
2949   /**
2950    * Adds to <code>group</code> a branch matching <code>homeObject</code> at a given <code>index</code>.
2951    * If <code>index</code> is equal to -1, <code>homeObject</code> will be added at the end of the group.
2952    */
addObject(Group group, Selectable homeObject, int index, boolean listenToHomeUpdates, boolean waitForLoading)2953   private Node addObject(Group group, Selectable homeObject, int index,
2954                          boolean listenToHomeUpdates, boolean waitForLoading) {
2955     Object3DBranch object3D = createObject3D(homeObject, waitForLoading);
2956     if (listenToHomeUpdates) {
2957       this.homeObjects.put(homeObject, object3D);
2958     }
2959     if (index == -1) {
2960       group.addChild(object3D);
2961     } else {
2962       group.insertChild(object3D, index);
2963     }
2964     clearPrintedImageCache();
2965     return object3D;
2966   }
2967 
2968   /**
2969    * Adds to <code>group</code> a branch matching <code>homeObject</code> or its children if the piece is a group of furniture.
2970    */
addPieceOfFurniture(Group group, HomePieceOfFurniture piece, boolean listenToHomeUpdates, boolean waitForLoading)2971   private void addPieceOfFurniture(Group group, HomePieceOfFurniture piece, boolean listenToHomeUpdates, boolean waitForLoading) {
2972     if (piece instanceof HomeFurnitureGroup) {
2973       for (HomePieceOfFurniture child : ((HomeFurnitureGroup)piece).getFurniture()) {
2974         addPieceOfFurniture(group, child, listenToHomeUpdates, waitForLoading);
2975       }
2976     } else {
2977       addObject(group, piece, listenToHomeUpdates, waitForLoading);
2978     }
2979   }
2980 
2981   /**
2982    * Returns the 3D object matching the given home object. If <code>waitForLoading</code>
2983    * is <code>true</code> the resources used by the returned 3D object should be ready to be displayed.
2984    * @deprecated Subclasses which used to override this method must be updated to create an instance of
2985    *    a {@link Object3DFactory factory} and give it as parameter to the constructor of this class.
2986    */
createObject3D(Selectable homeObject, boolean waitForLoading)2987   private Object3DBranch createObject3D(Selectable homeObject,
2988                                         boolean waitForLoading) {
2989     return (Object3DBranch)this.object3dFactory.createObject3D(this.home, homeObject, waitForLoading);
2990   }
2991 
2992   /**
2993    * Detaches from the scene the branch matching <code>homeObject</code>.
2994    */
deleteObject(Selectable homeObject)2995   private void deleteObject(Selectable homeObject) {
2996     this.homeObjects.get(homeObject).detach();
2997     this.homeObjects.remove(homeObject);
2998     if (this.homeObjectsToUpdate != null
2999         && this.homeObjectsToUpdate.contains(homeObject)) {
3000       this.homeObjectsToUpdate.remove(homeObject);
3001     }
3002     clearPrintedImageCache();
3003   }
3004 
3005   /**
3006    * Detaches from the scene the branches matching <code>piece</code> or its children if it's a group.
3007    */
deletePieceOfFurniture(HomePieceOfFurniture piece)3008   private void deletePieceOfFurniture(HomePieceOfFurniture piece) {
3009     if (piece instanceof HomeFurnitureGroup) {
3010       for (HomePieceOfFurniture child : ((HomeFurnitureGroup)piece).getFurniture()) {
3011         deletePieceOfFurniture(child);
3012       }
3013     } else {
3014       deleteObject(piece);
3015     }
3016   }
3017 
3018   /**
3019    * Updates 3D <code>objects</code> later. Should be invoked from Event Dispatch Thread.
3020    */
updateObjects(Collection<? extends Selectable> objects)3021   private void updateObjects(Collection<? extends Selectable> objects) {
3022     if (this.homeObjectsToUpdate != null) {
3023       this.homeObjectsToUpdate.addAll(objects);
3024     } else {
3025       this.homeObjectsToUpdate = new HashSet<Selectable>(objects);
3026       // Invoke later the update of objects of homeObjectsToUpdate
3027       EventQueue.invokeLater(new Runnable () {
3028         public void run() {
3029           for (Selectable object : homeObjectsToUpdate) {
3030             Object3DBranch objectBranch = homeObjects.get(object);
3031             // Check object wasn't deleted since updateObjects call
3032             if (objectBranch != null) {
3033               objectBranch.update();
3034             }
3035           }
3036           homeObjectsToUpdate = null;
3037         }
3038       });
3039     }
3040     clearPrintedImageCache();
3041     this.approximateHomeBoundsCache = null;
3042   }
3043 
3044   /**
3045    * Updates walls that may intersect from the given doors or window.
3046    */
updateIntersectingWalls(HomePieceOfFurniture .... doorOrWindows)3047   private void updateIntersectingWalls(HomePieceOfFurniture ... doorOrWindows) {
3048     Collection<Wall> walls = this.home.getWalls();
3049     int wallCount = 0;
3050     if (this.homeObjectsToUpdate != null) {
3051       for (Selectable object : this.homeObjectsToUpdate) {
3052         if (object instanceof Wall) {
3053           wallCount++;
3054         }
3055       }
3056     }
3057     // Check if some more walls may require an update
3058     if (wallCount != walls.size()) {
3059       List<Wall> updatedWalls = new ArrayList<Wall>();
3060       Rectangle2D doorOrWindowBounds = null;
3061       // Compute the approximate bounds of the doors and windows
3062       for (HomePieceOfFurniture doorOrWindow : doorOrWindows) {
3063         float [][] points = doorOrWindow.getPoints();
3064         if (doorOrWindowBounds == null) {
3065           doorOrWindowBounds = new Rectangle2D.Float(points [0][0], points [0][1], 0, 0);
3066         } else {
3067           doorOrWindowBounds.add(points [0][0], points [0][1]);
3068         }
3069         for (int i = 1; i < points.length; i++) {
3070           doorOrWindowBounds.add(points [i][0], points [i][1]);
3071         }
3072       }
3073       // Search walls that intersect the bounds
3074       for (Wall wall : walls) {
3075         if (wall.intersectsRectangle((float)doorOrWindowBounds.getX(), (float)doorOrWindowBounds.getY(),
3076             (float)doorOrWindowBounds.getX() + (float)doorOrWindowBounds.getWidth(),
3077             (float)doorOrWindowBounds.getY() + (float)doorOrWindowBounds.getHeight())) {
3078           updatedWalls.add(wall);
3079         }
3080       }
3081       updateObjects(updatedWalls);
3082     }
3083   }
3084 
3085   /**
3086    * Updates <code>wall</code> geometry and the walls at its end or start.
3087    */
updateWall(Wall wall)3088   private void updateWall(Wall wall) {
3089     Collection<Wall> wallsToUpdate = new ArrayList<Wall>(3);
3090     wallsToUpdate.add(wall);
3091     if (wall.getWallAtStart() != null) {
3092       wallsToUpdate.add(wall.getWallAtStart());
3093     }
3094     if (wall.getWallAtEnd() != null) {
3095       wallsToUpdate.add(wall.getWallAtEnd());
3096     }
3097     updateObjects(wallsToUpdate);
3098   }
3099 
3100   /**
3101    * Updates the <code>object</code> scope under light later. Should be invoked from Event Dispatch Thread.
3102    */
updateObjectsLightScope(Collection<? extends Selectable> objects)3103   private void updateObjectsLightScope(Collection<? extends Selectable> objects) {
3104     if (home.getEnvironment().getSubpartSizeUnderLight() > 0) {
3105       if (this.lightScopeObjectsToUpdate != null) {
3106         if (objects == null) {
3107           this.lightScopeObjectsToUpdate.clear();
3108           this.lightScopeObjectsToUpdate.add(null);
3109         } else if (!this.lightScopeObjectsToUpdate.contains(null)) {
3110           this.lightScopeObjectsToUpdate.addAll(objects);
3111         }
3112       } else {
3113         this.lightScopeObjectsToUpdate = new HashSet<Selectable>();
3114         if (objects == null) {
3115           this.lightScopeObjectsToUpdate.add(null);
3116         } else {
3117           this.lightScopeObjectsToUpdate.addAll(objects);
3118         }
3119         // Invoke later the update of objects of lightScopeObjectsToUpdate
3120         EventQueue.invokeLater(new Runnable () {
3121           public void run() {
3122             if (lightScopeObjectsToUpdate.contains(null)) {
3123               subpartSizeListener.propertyChange(null);
3124             } else if (home.getEnvironment().getSubpartSizeUnderLight() > 0) {
3125               Area lightScopeOutsideWallsArea = getLightScopeOutsideWallsArea();
3126               for (Selectable object : lightScopeObjectsToUpdate) {
3127                 Group object3D = homeObjects.get(object);
3128                 if (object3D instanceof HomePieceOfFurniture3D) {
3129                   // Add the direct parent of the shape that will be added once loaded
3130                   // otherwise scope won't be updated automatically
3131                   object3D = (Group)object3D.getChild(0);
3132                 }
3133                 // Check object wasn't deleted since updateObjects call
3134                 if (object3D != null) {
3135                   // Add item to scope if one of its points don't belong to lightScopeOutsideWallsArea
3136                   boolean objectInOutsideLightScope = false;
3137                   for (float [] point : object.getPoints()) {
3138                     if (!lightScopeOutsideWallsArea.contains(point [0], point [1])) {
3139                       objectInOutsideLightScope = true;
3140                       break;
3141                     }
3142                   }
3143                   for (Light light : sceneLights) {
3144                     if (light instanceof DirectionalLight) {
3145                       if (objectInOutsideLightScope && light.indexOfScope(object3D) == -1) {
3146                         light.addScope(object3D);
3147                       } else if (!objectInOutsideLightScope && light.indexOfScope(object3D) != -1) {
3148                         light.removeScope(object3D);
3149                       }
3150                     }
3151                   }
3152                 }
3153               }
3154             }
3155             lightScopeObjectsToUpdate = null;
3156           }
3157         });
3158       }
3159     }
3160   }
3161 
3162   /**
3163    * Adds to <code>homeRoot</code> shapes matching the shadow of furniture at their level.
3164    */
addShadowOnFloor(Group homeRoot, Map<HomePieceOfFurniture, Node> pieces3D)3165   private void addShadowOnFloor(Group homeRoot, Map<HomePieceOfFurniture, Node> pieces3D) {
3166     Comparator<Level> levelComparator = new Comparator<Level>() {
3167         public int compare(Level level1, Level level2) {
3168           return Float.compare(level1.getElevation(), level2.getElevation());
3169         }
3170       };
3171     Map<Level, Area> areasOnLevel = new TreeMap<Level, Area>(levelComparator);
3172     // Compute union of the areas of pieces at ground level that are not lights, doors or windows
3173     for (Map.Entry<HomePieceOfFurniture, Node> object3DEntry : pieces3D.entrySet()) {
3174       if (object3DEntry.getKey() instanceof HomePieceOfFurniture) {
3175         HomePieceOfFurniture piece = object3DEntry.getKey();
3176         // This operation can be lengthy, so give up if thread is interrupted
3177         if (Thread.currentThread().isInterrupted()) {
3178           return;
3179         }
3180         if (piece.getElevation() == 0
3181             && !piece.isDoorOrWindow()
3182             && !(piece instanceof com.eteks.sweethome3d.model.Light)) {
3183           Area pieceAreaOnFloor = ModelManager.getInstance().getAreaOnFloor(object3DEntry.getValue());
3184           Level level = piece.getLevel();
3185           if (piece.getLevel() == null) {
3186             level = new Level("Dummy", 0, 0, 0);
3187           }
3188           if (level.isViewableAndVisible()) {
3189             Area areaOnLevel = areasOnLevel.get(level);
3190             if (areaOnLevel == null) {
3191               areaOnLevel = new Area();
3192               areasOnLevel.put(level, areaOnLevel);
3193             }
3194             areaOnLevel.add(pieceAreaOnFloor);
3195           }
3196         }
3197       }
3198     }
3199 
3200     // Create the 3D shape matching computed areas
3201     Shape3D shadow = new Shape3D();
3202     for (Map.Entry<Level, Area> levelArea : areasOnLevel.entrySet()) {
3203       List<Point3f> coords = new ArrayList<Point3f>();
3204       List<Integer> stripCounts = new ArrayList<Integer>();
3205       int pointsCount = 0;
3206       float [] modelPoint = new float[2];
3207       for (PathIterator it = levelArea.getValue().getPathIterator(null); !it.isDone(); ) {
3208         if (it.currentSegment(modelPoint) == PathIterator.SEG_CLOSE) {
3209           stripCounts.add(pointsCount);
3210           pointsCount = 0;
3211         } else {
3212           coords.add(new Point3f(modelPoint [0], levelArea.getKey().getElevation() + 0.49f, modelPoint [1]));
3213           pointsCount++;
3214         }
3215         it.next();
3216       }
3217 
3218       if (coords.size() > 0) {
3219         GeometryInfo geometryInfo = new GeometryInfo(GeometryInfo.POLYGON_ARRAY);
3220         geometryInfo.setCoordinates (coords.toArray(new Point3f [coords.size()]));
3221         int [] stripCountsArray = new int [stripCounts.size()];
3222         for (int i = 0; i < stripCountsArray.length; i++) {
3223           stripCountsArray [i] = stripCounts.get(i);
3224         }
3225         geometryInfo.setStripCounts(stripCountsArray);
3226         shadow.addGeometry(geometryInfo.getIndexedGeometryArray());
3227       }
3228     }
3229 
3230     Appearance shadowAppearance = new Appearance();
3231     shadowAppearance.setColoringAttributes(new ColoringAttributes(new Color3f(), ColoringAttributes.SHADE_FLAT));
3232     shadowAppearance.setTransparencyAttributes(new TransparencyAttributes(TransparencyAttributes.NICEST, 0.7f));
3233     shadow.setAppearance(shadowAppearance);
3234     homeRoot.addChild(shadow);
3235   }
3236 }
3237