Package com.eteks.sweethome3d.swing

Source Code of com.eteks.sweethome3d.swing.HomeComponent3D

/*
* HomeComponent3D.java 24 ao?t 2006
*
* Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks <info@eteks.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
package com.eteks.sweethome3d.swing;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Composite;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.FilteredImageSource;
import java.awt.image.RGBImageFilter;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.media.j3d.Alpha;
import javax.media.j3d.AmbientLight;
import javax.media.j3d.Appearance;
import javax.media.j3d.Background;
import javax.media.j3d.BoundingBox;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.ColoringAttributes;
import javax.media.j3d.DirectionalLight;
import javax.media.j3d.Geometry;
import javax.media.j3d.GraphicsConfigTemplate3D;
import javax.media.j3d.Group;
import javax.media.j3d.IllegalRenderingStateException;
import javax.media.j3d.J3DGraphics2D;
import javax.media.j3d.Light;
import javax.media.j3d.Link;
import javax.media.j3d.Node;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Texture;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TransformInterpolator;
import javax.media.j3d.TransparencyAttributes;
import javax.media.j3d.View;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.RepaintManager;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.border.Border;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.MouseInputAdapter;
import javax.vecmath.Color3f;
import javax.vecmath.Point3d;
import javax.vecmath.Point3f;
import javax.vecmath.TexCoord2f;
import javax.vecmath.Vector3f;

import com.eteks.sweethome3d.j3d.Component3DManager;
import com.eteks.sweethome3d.j3d.Ground3D;
import com.eteks.sweethome3d.j3d.ModelManager;
import com.eteks.sweethome3d.j3d.Object3DBranch;
import com.eteks.sweethome3d.j3d.Object3DBranchFactory;
import com.eteks.sweethome3d.j3d.TextureManager;
import com.eteks.sweethome3d.model.Camera;
import com.eteks.sweethome3d.model.CollectionEvent;
import com.eteks.sweethome3d.model.CollectionListener;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.HomeEnvironment;
import com.eteks.sweethome3d.model.HomeFurnitureGroup;
import com.eteks.sweethome3d.model.HomePieceOfFurniture;
import com.eteks.sweethome3d.model.HomeTexture;
import com.eteks.sweethome3d.model.Room;
import com.eteks.sweethome3d.model.Selectable;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.model.Wall;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.viewcontroller.HomeController3D;
import com.eteks.sweethome3d.viewcontroller.Object3DFactory;
import com.sun.j3d.exp.swing.JCanvas3D;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.universe.SimpleUniverse;
import com.sun.j3d.utils.universe.Viewer;
import com.sun.j3d.utils.universe.ViewingPlatform;

/**
* A component that displays home walls, rooms and furniture with Java 3D.
* @author Emmanuel Puybaret
*/
public class HomeComponent3D extends JComponent implements com.eteks.sweethome3d.viewcontroller.View, Printable {
  private enum ActionType {MOVE_CAMERA_FORWARD, MOVE_CAMERA_FAST_FORWARD, MOVE_CAMERA_BACKWARD, MOVE_CAMERA_FAST_BACKWARD, 
      ROTATE_CAMERA_YAW_LEFT, ROTATE_CAMERA_YAW_FAST_LEFT, ROTATE_CAMERA_YAW_RIGHT, ROTATE_CAMERA_YAW_FAST_RIGHT,
      ROTATE_CAMERA_PITCH_UP, ROTATE_CAMERA_PITCH_DOWN,
      ELEVATE_CAMERA_UP, ELEVATE_CAMERA_FAST_UP, ELEVATE_CAMERA_DOWN, ELEVATE_CAMERA_FAST_DOWN}
 
  private final Home                               home;
  private final boolean                            displayShadowOnFloor;
  private final Object3DFactory                    object3dFactory;
  private final Map<Selectable, Object3DBranch>    homeObjects = new HashMap<Selectable, Object3DBranch>();
  private Collection<Selectable>                   homeObjectsToUpdate;
  private Component                                component3D;
  private SimpleUniverse                           onscreenUniverse;
  private Camera                                   camera;
  // Listeners bound to home that updates 3D scene objects
  private PropertyChangeListener                   cameraChangeListener;
  private PropertyChangeListener                   homeCameraListener;
  private PropertyChangeListener                   skyColorListener;
  private PropertyChangeListener                   groundColorAndTextureListener;
  private PropertyChangeListener                   lightColorListener;
  private PropertyChangeListener                   wallsAlphaListener;
  private PropertyChangeListener                   drawingModeListener;
  private CollectionListener<Wall>                 wallListener;
  private PropertyChangeListener                   wallChangeListener;
  private CollectionListener<HomePieceOfFurniture> furnitureListener;
  private PropertyChangeListener                   furnitureChangeListener;
  private CollectionListener<Room>                 roomListener;
  private PropertyChangeListener                   roomChangeListener;
  // Offscreen printed image cache
  // Creating an offscreen buffer is a quite lengthy operation so we keep the last printed image in this field
  // This image should be set to null each time the 3D view changes
  private BufferedImage                            printedImage;
  private SimpleUniverse                           offscreenUniverse;
 
  private JComponent                               navigationPanel;
  private ComponentListener                        navigationPanelListener;
  private BufferedImage                            navigationPanelImage;
 
  /**
   * Creates a 3D component that displays <code>home</code> walls, rooms and furniture,
   * with no controller.
   * @throws IllegalRenderingStateException  if the canvas 3D displayed
   *             by this component couldn't be created.
   */
  public HomeComponent3D(Home home) {
    this(home, null)
  }
 
  /**
   * Creates a 3D component that displays <code>home</code> walls, rooms and furniture.
   * @throws IllegalRenderingStateException  if the canvas 3D displayed
   *             by this component couldn't be created.
   */
  public HomeComponent3D(Home home, HomeController3D controller) {
    this(home, null, controller);
  }

  /**
   * Creates a 3D component that displays <code>home</code> walls, rooms and furniture,
   * with shadows on the floor.
   * @throws IllegalRenderingStateException  if the canvas 3D displayed
   *             by this component couldn't be created.
   */
  public HomeComponent3D(Home home,
                         UserPreferences  preferences,
                         boolean displayShadowOnFloor) {
    this(home, preferences, new Object3DBranchFactory(), displayShadowOnFloor, null)
  }
 
  /**
   * Creates a 3D component that displays <code>home</code> walls, rooms and furniture.
   * @throws IllegalRenderingStateException  if the canvas 3D displayed
   *            by this component couldn't be created.
   */
  public HomeComponent3D(Home home,
                         UserPreferences  preferences,
                         HomeController3D controller) {
    this(home, preferences, new Object3DBranchFactory(), false, controller);   
  }

  /**
   * Creates a 3D component that displays <code>home</code> walls, rooms and furniture.
   * @param home the home to display in this component
   * @param preferences user preferences
   * @param object3DFactory a factory able to create 3D objects from <code>home</code> items.
   *            The {@link Object3DFactory#createObject3D(Home, Selectable, boolean) createObject3D} of
   *            this factory is expected to return an instance of {@link Object3DBranch} in current implementation.
   * @param controller the controller that manages modifications in <code>home</code>.          
   * @throws IllegalRenderingStateException  if the canvas 3D displayed
   *            by this component couldn't be created.
   */
  public HomeComponent3D(Home home,
                         UserPreferences  preferences,
                         Object3DFactory  object3DFactory,
                         HomeController3D controller) {
    this(home, preferences, object3DFactory, false, controller);   
  }

  /**
   * Creates a 3D component that displays <code>home</code> walls, rooms and furniture.
   * @throws IllegalRenderingStateException  if the canvas 3D displayed
   *            by this component couldn't be created.
   */
  private HomeComponent3D(Home home,
                          UserPreferences  preferences,
                          Object3DFactory  object3DFactory,
                          boolean displayShadowOnFloor,
                          HomeController3D controller) {
    this.home = home;
    this.displayShadowOnFloor = displayShadowOnFloor;
    this.object3dFactory = object3DFactory;

    if (Boolean.valueOf(System.getProperty("com.eteks.sweethome3d.j3d.useOffScreen3DView", "false"))) {
      GraphicsConfigTemplate3D gc = new GraphicsConfigTemplate3D();
      gc.setSceneAntialiasing(GraphicsConfigTemplate3D.PREFERRED);
      this.component3D = new JCanvas3D(gc) {
          public void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.drawImage(navigationPanelImage, 0, 0, this);
          }
        };
      this.component3D.setSize(1, 1);
    } else {
      this.component3D = Component3DManager.getInstance().getOnscreenCanvas3D(new Component3DManager.RenderingObserver() {       
        public void canvas3DSwapped(Canvas3D canvas3D) {
        }
       
        public void canvas3DPreRendered(Canvas3D canvas3D) {
        }
       
        public void canvas3DPostRendered(Canvas3D canvas3D) {
          // Copy reference to navigation panel image to avoid concurrency problems
          // if it's modified in the EDT while this method draws it
          BufferedImage navigationPanelImage = HomeComponent3D.this.navigationPanelImage;
          // Render navigation panel upon canvas 3D if it exists
          if (navigationPanelImage != null) {
            J3DGraphics2D g2D = canvas3D.getGraphics2D();
            g2D.drawImage(navigationPanelImage, null, 0, 0);
            g2D.flush(true);
          }
        }
      });
    }
    JPanel canvasPanel = new JPanel(new LayoutManager() {
        public void addLayoutComponent(String name, Component comp) {
        }
       
        public void removeLayoutComponent(Component comp) {
        }
       
        public Dimension preferredLayoutSize(Container parent) {
          return component3D.getPreferredSize();
        }
       
        public Dimension minimumLayoutSize(Container parent) {
          return component3D.getMinimumSize();
        }
       
        public void layoutContainer(Container parent) {
          component3D.setBounds(0, 0, Math.max(1, parent.getWidth()), Math.max(1, parent.getHeight()));
          if (navigationPanel != null
              && navigationPanel.isVisible()) {
            // Ensure that navigationPanel is always in top corner            
            Dimension preferredSize = navigationPanel.getPreferredSize();
            navigationPanel.setBounds(0, 0, preferredSize.width, preferredSize.height);
          }
        }
      });
   
    canvasPanel.add(this.component3D);   
    setLayout(new GridLayout());
    add(canvasPanel);

    if (controller != null) {
      addMouseListeners(controller, this.component3D);
      if (preferences != null
          && (!OperatingSystem.isMacOSX()
              || OperatingSystem.isMacOSXLeopardOrSuperior())) {
        // No support for navigation panel under Mac OS X Tiger
        // (too unstable, may crash system at 3D view resizing)
        this.navigationPanel = createNavigationPanel(home, preferences, controller);
        setNavigationPanelVisible(preferences.isNavigationPanelVisible() && isVisible());
        preferences.addPropertyChangeListener(UserPreferences.Property.NAVIGATION_PANEL_VISIBLE,
            new NavigationPanelChangeListener(this, controller));
      }
      createActions(controller);
      installKeyboardActions();
      // Let this component manage focus
      setFocusable(true);
      SwingTools.installFocusBorder(this);
    }

    // Add an ancestor listener to create canvas universe once this component is made visible
    // and clean up universe once its parent frame is disposed
    addAncestorListener(this.component3D, displayShadowOnFloor);
  }

  /**
   * Adds an ancestor listener to this component to manage canvas universe
   * creation and clean up. 
   */
  private void addAncestorListener(final Component component3D,
                                   final boolean displayShadowOnFloor) {
    addAncestorListener(new AncestorListener() {       
        public void ancestorAdded(AncestorEvent event) {
          if (offscreenUniverse != null) {
            throw new IllegalStateException("Can't listen to home changes offscreen and onscreen at the same time");
          }
          onscreenUniverse = createUniverse(displayShadowOnFloor, true, false);
          // Bind universe to canvas3D
          onscreenUniverse.getViewer().getView().addCanvas3D(component3D instanceof Canvas3D
              ? (Canvas3D)component3D
              : ((JCanvas3D)component3D).getOffscreenCanvas3D());
          component3D.setFocusable(false);
          updateNavigationPanelImage();
        }
       
        public void ancestorRemoved(AncestorEvent event) {
          if (onscreenUniverse != null) {
            onscreenUniverse.cleanup();
            removeHomeListeners();
            onscreenUniverse = null;
          }
        }
       
        public void ancestorMoved(AncestorEvent event) {
        }       
      });
  }

  @Override
  public void setVisible(boolean visible) {
    super.setVisible(visible);
    this.component3D.setVisible(visible);
  }
 
  /**
   * Preferences property listener bound to this component with a weak reference to avoid
   * strong link between preferences and this component. 
   */
  private static class NavigationPanelChangeListener implements PropertyChangeListener {
    private final WeakReference<HomeComponent3D>  homeComponent3D;

    public NavigationPanelChangeListener(HomeComponent3D homeComponent3D,
                                         HomeController3D controller) {
      this.homeComponent3D = new WeakReference<HomeComponent3D>(homeComponent3D);
    }
   
    public void propertyChange(PropertyChangeEvent ev) {
      // If home pane was garbage collected, remove this listener from preferences
      HomeComponent3D homeComponent3D = this.homeComponent3D.get();
      if (homeComponent3D == null) {
        ((UserPreferences)ev.getSource()).removePropertyChangeListener(
            UserPreferences.Property.NAVIGATION_PANEL_VISIBLE, this);
      } else {
        homeComponent3D.setNavigationPanelVisible((Boolean)ev.getNewValue() && homeComponent3D.isVisible());
      }
    }
  }

  /**
   * Returns the component displayed as navigation panel by this 3D view.
   */
  private JComponent createNavigationPanel(Home home,
                                           UserPreferences preferences,
                                           HomeController3D controller) {
    JPanel navigationPanel = new JPanel(new GridBagLayout()) {
        @Override
        public void applyComponentOrientation(ComponentOrientation o) {
          // Ignore orientation
        }
      };
    String navigationPanelIconPath = preferences.getLocalizedString(HomeComponent3D.class, "navigationPanel.icon");
    final ImageIcon nagivationPanelIcon = navigationPanelIconPath.length() > 0
        ? new ImageIcon(HomeComponent3D.class.getResource(navigationPanelIconPath))
        : null;
    navigationPanel.setBorder(new Border() {
        public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
          if (nagivationPanelIcon != null) {
            nagivationPanelIcon.paintIcon(c, g, x, y);
          } else {
            // Draw a surrounding oval if no navigation panel icon is defined
            Graphics2D g2D = (Graphics2D)g;
            g2D.setColor(Color.BLACK);
            g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2D.drawOval(x + 3, y + 3, width - 6, height - 6);
          }
        }
 
        public Insets getBorderInsets(Component c) {
          return new Insets(2, 2, 2, 2);
        }
 
        public boolean isBorderOpaque() {
          return false;
        }
      });
    navigationPanel.setOpaque(false);
    navigationPanel.add(new NavigationButton(0, -(float)Math.PI / 36, 0, "TURN_LEFT", preferences, controller),
        new GridBagConstraints(0, 1, 1, 2, 0, 0,
            GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 3, 0, 0), 0, 0));
    navigationPanel.add(new NavigationButton(12.5f, 0, 0, "GO_FORWARD", preferences, controller),
        new GridBagConstraints(1, 0, 1, 1, 0, 0,
            GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(3, 0, 0, 0), 0, 0));
    navigationPanel.add(new NavigationButton(0, (float)Math.PI / 36, 0, "TURN_RIGHT", preferences, controller),
        new GridBagConstraints(2, 1, 1, 2, 0, 0,
            GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 2), 0, 0));
    navigationPanel.add(new NavigationButton(-12.5f, 0, 0, "GO_BACKWARD", preferences, controller),
        new GridBagConstraints(1, 3, 1, 1, 0, 0,
            GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 2, 0), 0, 0));
    navigationPanel.add(new NavigationButton(0, 0, -(float)Math.PI / 100, "TURN_UP", preferences, controller),
        new GridBagConstraints(1, 1, 1, 1, 0, 0,
            GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(1, 1, 1, 1), 0, 0));
    navigationPanel.add(new NavigationButton(0, 0, (float)Math.PI / 100, "TURN_DOWN", preferences, controller),
        new GridBagConstraints(1, 2, 1, 1, 0, 0,
            GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 1, 0), 0, 0));
    return navigationPanel;
  }
 
  /**
   * An icon button that changes camera location and angles when pressed.
   */
  private static class NavigationButton extends JButton {
    private boolean shiftDown;

    public NavigationButton(final float moveDelta,
                            final float yawDelta,
                            final float pitchDelta,
                            String actionName,
                            UserPreferences preferences,
                            final HomeController3D controller) {
      super(new ResourceAction(preferences, HomeComponent3D.class, actionName, true) {
          @Override
          public void actionPerformed(ActionEvent ev) {
            // Manage auto repeat button with mouse listener
          }
        });
      // Create a darker press icon
      setPressedIcon(new ImageIcon(createImage(new FilteredImageSource(
          ((ImageIcon)getIcon()).getImage().getSource(),
          new RGBImageFilter() {
            {
              canFilterIndexColorModel = true;
            }

            public int filterRGB (int x, int y, int rgb) {
              // Return darker color
              int alpha = rgb & 0xFF000000;
              int darkerRed = ((rgb & 0xFF0000) >> 1) & 0xFF0000;
              int darkerGreen  = ((rgb & 0x00FF00) >> 1) & 0x00FF00;
              int darkerBlue  = (rgb & 0x0000FF) >> 1;
              return alpha | darkerRed | darkerGreen | darkerBlue;
            }
          }))));
    
      // Track shift key press
      addMouseMotionListener(new MouseMotionAdapter() {
          @Override
          public void mouseDragged(MouseEvent ev) {
            shiftDown = ev.isShiftDown();
          }
        });
      addMouseListener(new MouseAdapter() {
          @Override
          public void mousePressed(MouseEvent ev) {
            shiftDown = ev.isShiftDown();
            SwingUtilities.getAncestorOfClass(HomeComponent3D.class,
                NavigationButton.this).requestFocusInWindow();
          }
        });
     
      // Create a timer that will update camera angles and location
      final Timer timer = new Timer(50, new ActionListener() {
          public void actionPerformed(ActionEvent ev) {
            controller.moveCamera(shiftDown ? moveDelta : moveDelta / 5);
            controller.rotateCameraYaw(shiftDown ? yawDelta : yawDelta / 5);
            controller.rotateCameraPitch(pitchDelta);
          }
        });
      timer.setInitialDelay(0);
     
      // Update camera when button is armed
      addChangeListener(new ChangeListener() {
          public void stateChanged(ChangeEvent ev) {
            if (getModel().isArmed()
                && !timer.isRunning()) {
              timer.restart();
            } else if (!getModel().isArmed()
                       && timer.isRunning()) {
              timer.stop();
           
          }
        });
      setFocusable(false);
      setBorder(null);
      setContentAreaFilled(false);
      // Force preferred size to ensure button isn't larger
      setPreferredSize(new Dimension(getIcon().getIconWidth(), getIcon().getIconHeight()));
      addPropertyChangeListener(JButton.ICON_CHANGED_PROPERTY, new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            // Reset border when icon is reset after a resource action change
            setBorder(null);
          }
        });
    }
  }

  /**
   * Sets the component that will be drawn upon the heavyweight 3D component shown by this component.
   * Mouse events will targeted to the navigation panel when needed.
   * Supports transparent components.
   */
  private void setNavigationPanelVisible(boolean visible) {
    if (this.navigationPanel != null) {
      this.navigationPanel.setVisible(visible);
      if (visible) {
        // Add a component listener that updates navigation panel image
        this.navigationPanelListener = new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent ev) {
              updateNavigationPanelImage();         
            }
           
            @Override
            public void componentMoved(ComponentEvent e) {
              updateNavigationPanelImage();         
            }
          };
        this.navigationPanel.addComponentListener(this.navigationPanelListener);
        // Add the navigation panel to this component to be able to paint it
        // but show it behind canvas 3D
        this.component3D.getParent().add(this.navigationPanel);   
      } else {
        this.navigationPanel.removeComponentListener(this.navigationPanelListener);
        if (this.navigationPanel.getParent() != null) {
          this.navigationPanel.getParent().remove(this.navigationPanel);
        }
      }
      revalidate();
      updateNavigationPanelImage();         
      this.component3D.repaint();
    }
  }
 
  /**
   * Updates the image of the components that may overlap canvas 3D
   * (with a Z order smaller than the one of the canvas 3D).
   */
  private void updateNavigationPanelImage() {
    if (this.navigationPanel != null
        && this.navigationPanel.isVisible()) {
      Rectangle componentBounds = this.navigationPanel.getBounds();
      Rectangle imageSize = new Rectangle(this.component3D.getX(), this.component3D.getY());
      imageSize.add(componentBounds.x + componentBounds.width,
          componentBounds.y + componentBounds.height);
      if (!imageSize.isEmpty()) {
        BufferedImage updatedImage = this.navigationPanelImage;
        // Consider that no navigation panel image is available
        // while it's updated
        this.navigationPanelImage = null;       
        Graphics2D g2D;
        if (updatedImage == null
            || updatedImage.getWidth() != imageSize.width
            || updatedImage.getHeight() != imageSize.height) {
          updatedImage = new BufferedImage(
              imageSize.width, imageSize.height, BufferedImage.TYPE_INT_ARGB);
          g2D = (Graphics2D)updatedImage.getGraphics();
        } else {
          // Clear image
          g2D = (Graphics2D)updatedImage.getGraphics();
          Composite oldComposite = g2D.getComposite();
          g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0));
          g2D.fill(new Rectangle2D.Double(0, 0, imageSize.width, imageSize.height));
          g2D.setComposite(oldComposite);
        }
        this.navigationPanel.paintAll(g2D);
        g2D.dispose();
        // Navigation panel image ready to be displayed
        this.navigationPanelImage = updatedImage;
        return;
      }
    }
    this.navigationPanelImage = null;
  }

  /**
   * Returns a new 3D universe that displays <code>home</code> objects.
   */
  private SimpleUniverse createUniverse(boolean displayShadowOnFloor,
                                        boolean listenToHomeUpdates,
                                        boolean waitForLoading) {
    // Create a universe bound to no canvas 3D
    ViewingPlatform viewingPlatform = new ViewingPlatform();
    // Add an interpolator to view transform to get smooth transition
    TransformGroup viewPlatformTransform = viewingPlatform.getViewPlatformTransform();
    CameraInterpolator cameraInterpolator = new CameraInterpolator(viewPlatformTransform);
    cameraInterpolator.setSchedulingBounds(new BoundingSphere(new Point3d(), 1E7));
    viewPlatformTransform.addChild(cameraInterpolator);
    viewPlatformTransform.setCapability(TransformGroup.ALLOW_CHILDREN_READ);
   
    Viewer viewer = new Viewer(new Canvas3D [0]);
    SimpleUniverse universe = new SimpleUniverse(viewingPlatform, viewer);
   
    View view = viewer.getView();
    // Update field of view from current camera
    updateView(view, this.home.getCamera(), this.home.getTopCamera() == this.home.getCamera());
   
    // Update point of view from current camera
    updateViewPlatformTransform(viewPlatformTransform, this.home.getCamera(), false);
   
    // Add camera listeners to update later point of view from camera
    if (listenToHomeUpdates) {
      addCameraListeners(view, viewPlatformTransform);
    }
   
    // Link scene matching home to universe
    universe.addBranchGraph(createSceneTree(
        displayShadowOnFloor, listenToHomeUpdates, waitForLoading));
   
    return universe;
  }
 
  /**
   * Remove all listeners bound to home that updates 3D scene objects.
   */
  private void removeHomeListeners() {
    this.home.removePropertyChangeListener(Home.Property.CAMERA, this.homeCameraListener);
    HomeEnvironment homeEnvironment = this.home.getEnvironment();
    homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.SKY_COLOR, this.skyColorListener);
    homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.SKY_TEXTURE, this.skyColorListener);
    homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.GROUND_COLOR, this.groundColorAndTextureListener);
    homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.GROUND_TEXTURE, this.groundColorAndTextureListener);
    homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.LIGHT_COLOR, this.lightColorListener);
    homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.WALLS_ALPHA, this.wallsAlphaListener);
    homeEnvironment.removePropertyChangeListener(HomeEnvironment.Property.DRAWING_MODE, this.drawingModeListener);
    this.home.getCamera().removePropertyChangeListener(this.cameraChangeListener);
    this.home.removeWallsListener(this.wallListener);
    for (Wall wall : this.home.getWalls()) {
      wall.removePropertyChangeListener(this.wallChangeListener);
    }
    this.home.removeFurnitureListener(this.furnitureListener);
    for (HomePieceOfFurniture piece : this.home.getFurniture()) {
      piece.removePropertyChangeListener(this.furnitureChangeListener);
    }
    this.home.removeRoomsListener(this.roomListener);
    for (Room room : this.home.getRooms()) {
      room.removePropertyChangeListener(this.roomChangeListener);
    }
  }

  /**
   * Prints this component to make it fill <code>pageFormat</code> imageable size.
   */
  public int print(Graphics g, PageFormat pageFormat, int pageIndex) {
    if (pageIndex == 0) {    
      // Compute printed image size to render 3D view in 150 dpi
      double printSize = Math.min(pageFormat.getImageableWidth(),
          pageFormat.getImageableHeight());
      int printedImageSize = (int)(printSize / 72 * 150);
      if (this.printedImage == null
          || this.printedImage.getWidth() != printedImageSize) {
        try {         
          this.printedImage = getOffScreenImage(printedImageSize, printedImageSize);
        } catch (IllegalRenderingStateException ex) {
          // If off screen canvas failed, consider that 3D view page doesn't exist
          return NO_SUCH_PAGE;
        }
      }
 
      Graphics2D g2D = (Graphics2D)g.create();
      // Center the 3D view in component
      g2D.translate(pageFormat.getImageableX() + (pageFormat.getImageableWidth() - printSize) / 2,
          pageFormat.getImageableY() + (pageFormat.getImageableHeight() - printSize) / 2);
      double scale = printSize / printedImageSize;
      g2D.scale(scale, scale);
      g2D.drawImage(this.printedImage, 0, 0, this);
      g2D.dispose();

      return PAGE_EXISTS;
    } else {
      return NO_SUCH_PAGE;
    }
  }
 
  /**
   * Optimizes this component for the creation of a sequence of multiple off screen images.
   * Once off screen images are generated with {@link #getOffScreenImage(int, int) getOffScreenImage},
   * call {@link #endOffscreenImagesCreation() endOffscreenImagesCreation} method to free resources.
   */
  void startOffscreenImagesCreation() {
    if (this.offscreenUniverse == null) {
      if (this.onscreenUniverse != null) {
        throw new IllegalStateException("Can't listen to home changes offscreen and onscreen at the same time");
      }
      this.offscreenUniverse = createUniverse(this.displayShadowOnFloor, true, true);
      // Replace textures by clones because Java 3D doesn't accept all the time
      // to share textures between offscreen and onscreen environments
      Map<Texture, Texture> replacedTextures = new HashMap<Texture, Texture>();
      for (Enumeration it = this.offscreenUniverse.getLocale().getAllBranchGraphs(); it.hasMoreElements(); ) {
        cloneTexture((Node)it.nextElement(), replacedTextures);
      }     
    }
  }
 
  /**
   * Returns an image of the home viewed by this component at the given size.
   */
  public BufferedImage getOffScreenImage(int width, int height) {
    List<Selectable> selectedItems = this.home.getSelectedItems();
    SimpleUniverse offScreenImageUniverse = null;
    try {
      View view;
      if (this.offscreenUniverse == null) {
        offScreenImageUniverse = createUniverse(this.displayShadowOnFloor, false, true);
        view = offScreenImageUniverse.getViewer().getView();
        // Replace textures by clones because Java 3D doesn't accept all the time
        // to share textures between offscreen and onscreen environments
        Map<Texture, Texture> replacedTextures = new HashMap<Texture, Texture>();
        for (Enumeration it = offScreenImageUniverse.getLocale().getAllBranchGraphs(); it.hasMoreElements(); ) {
          cloneTexture((Node)it.nextElement(), replacedTextures);
        }     
      } else {
        view = this.offscreenUniverse.getViewer().getView();
      }
     
      // Empty temporarily selection to create the off screen image
      List<Selectable> emptySelection = Collections.emptyList();
      this.home.setSelectedItems(emptySelection);
      return Component3DManager.getInstance().getOffScreenImage(view, width, height);
    } finally {
      // Restore selection
      this.home.setSelectedItems(selectedItems);
      if (offScreenImageUniverse != null) {
        offScreenImageUniverse.cleanup();
      }
    }
  }
 
  /**
   * Replace the textures set on node shapes by clones.
   */
  private void cloneTexture(Node node, Map<Texture, Texture> replacedTextures) {
    if (node instanceof Group) {
      // Enumerate children
      Enumeration<?> enumeration = ((Group)node).getAllChildren();
      while (enumeration.hasMoreElements()) {
        cloneTexture((Node)enumeration.nextElement(), replacedTextures);
      }
    } else if (node instanceof Link) {
      cloneTexture(((Link)node).getSharedGroup(), replacedTextures);
    } else if (node instanceof Shape3D) {
      Appearance appearance = ((Shape3D)node).getAppearance();
      if (appearance != null) {
        Texture texture = appearance.getTexture();
        if (texture != null) {
          Texture replacedTexture = replacedTextures.get(texture);
          if (replacedTexture == null) {
            replacedTexture = (Texture)texture.cloneNodeComponent(false);
            replacedTextures.put(texture, replacedTexture);
          }
          appearance.setTexture(replacedTexture);
        }
      }
    }
  }

  /**
   * Frees unnecessary resources after the creation of a sequence of multiple offscreen images.
   */
  void endOffscreenImagesCreation() {
    if (this.offscreenUniverse != null) {
      this.offscreenUniverse.cleanup();
      removeHomeListeners();
      this.offscreenUniverse = null;
    }
  }
 
  /**
   * Adds listeners to home to update point of view from current camera.
   */
  private void addCameraListeners(final View view,
                                  final TransformGroup viewPlatformTransform) {
    this.cameraChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          // Update view transform later to avoid flickering in case of multiple camera changes
          EventQueue.invokeLater(new Runnable() {
            public void run() {
              updateView(view, home.getCamera(), home.getTopCamera() == home.getCamera());
              updateViewPlatformTransform(viewPlatformTransform, home.getCamera(), true);
            }
          });
        }
      };
    this.home.getCamera().addPropertyChangeListener(this.cameraChangeListener);
    this.homeCameraListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          updateView(view, home.getCamera(), home.getTopCamera() == home.getCamera());
          updateViewPlatformTransform(viewPlatformTransform, home.getCamera(), false);
          // Add camera change listener to new active camera
          ((Camera)ev.getOldValue()).removePropertyChangeListener(cameraChangeListener);
          home.getCamera().addPropertyChangeListener(cameraChangeListener);
        }
      };
    this.home.addPropertyChangeListener(Home.Property.CAMERA, this.homeCameraListener);
  }

  /**
   * Updates <code>view</code> from <code>camera</code> field of view.
   */
  private void updateView(View view, Camera camera, boolean topCamera) {
    float fieldOfView = camera.getFieldOfView();
    if (fieldOfView == 0) {
      fieldOfView = (float)(Math.PI * 63 / 180);
    }
    view.setFieldOfView(fieldOfView);
    double frontClipDistance;
    if (topCamera) {
      // Use a variable front clip distance for top camera
      // depending on the distance to scene center     
      frontClipDistance = 1 + Math.abs(camera.getZ() / Math.sin(camera.getPitch())) / 50;
    } else {
      // Use a variable front clip distance for observer camera
      // depending on the elevation (at an elevation higher than 3 m
      // back clip distance must be greater than 6000 or a black zone appears
      // at the horizon in off screen images)
      frontClipDistance = 2.5;
      if (camera.getZ() > 200) {
        frontClipDistance += (camera.getZ() - 200) / 50;
      }
    }
    // Update front and back clip distance to ensure their ratio is less than 3000
    view.setFrontClipDistance(frontClipDistance);
    view.setBackClipDistance(frontClipDistance * 3000);
    clearPrintedImageCache();
  }

  /**
   * Frees printed image kept in cache.
   */
  private void clearPrintedImageCache() {
    this.printedImage = null;
  }
 
  /**
   * Updates <code>viewPlatformTransform</code> transform from <code>camera</code> angles and location.
   */
  private void updateViewPlatformTransform(TransformGroup viewPlatformTransform,
                                           Camera camera, boolean updateWithAnimation) {
    if (updateWithAnimation) {
      // Get the camera interpolator
      CameraInterpolator cameraInterpolator =
          (CameraInterpolator)viewPlatformTransform.getChild(viewPlatformTransform.numChildren() - 1);
      cameraInterpolator.moveCamera(camera);
    } else {
      Transform3D transform = new Transform3D();
      updateViewPlatformTransform(transform, camera.getX(), camera.getY(),
          camera.getZ(), camera.getYaw(), camera.getPitch());
      viewPlatformTransform.setTransform(transform);
    }
    clearPrintedImageCache();
  }

  /**
   * An interpolator that computes smooth camera moves.
   */
  private class CameraInterpolator extends TransformInterpolator {
    private final ScheduledExecutorService scheduledExecutor;
    private Camera initialCamera;
    private Camera finalCamera;
   
    public CameraInterpolator(TransformGroup transformGroup) {
      this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
      setTarget(transformGroup);
    }
   
    /**
     * Moves the camera to a new location.
     */
    public void moveCamera(Camera finalCamera) {
      if (this.finalCamera == null
          || this.finalCamera.getX() != finalCamera.getX()
          || this.finalCamera.getY() != finalCamera.getY()
          || this.finalCamera.getZ() != finalCamera.getZ()
          || this.finalCamera.getYaw() != finalCamera.getYaw()
          || this.finalCamera.getPitch() != finalCamera.getPitch()) {
        synchronized (this) {
          Alpha alpha = getAlpha();
          if (alpha == null || alpha.finished()) {
            this.initialCamera = new Camera(camera.getX(), camera.getY(), camera.getZ(),
                camera.getYaw(), camera.getPitch(), camera.getFieldOfView());
          } else if (alpha.value() < 0.3) {
            Transform3D finalTransformation = new Transform3D();
            // Jump directly to final location
            updateViewPlatformTransform(finalTransformation, this.finalCamera.getX(), this.finalCamera.getY(), this.finalCamera.getZ(),
                this.finalCamera.getYaw(), this.finalCamera.getPitch());
            getTarget().setTransform(finalTransformation);
            this.initialCamera = this.finalCamera;
          } else {
            // Compute initial location from current alpha value
            this.initialCamera = new Camera(this.initialCamera.getX() + (this.finalCamera.getX() - this.initialCamera.getX()) * alpha.value(),
                this.initialCamera.getY() + (this.finalCamera.getY() - this.initialCamera.getY()) * alpha.value(),
                this.initialCamera.getZ() + (this.finalCamera.getZ() - this.initialCamera.getZ()) * alpha.value(),
                this.initialCamera.getYaw() + (this.finalCamera.getYaw() - this.initialCamera.getYaw()) * alpha.value(),
                this.initialCamera.getPitch() + (this.finalCamera.getPitch() - this.initialCamera.getPitch()) * alpha.value(),
                finalCamera.getFieldOfView());
          }
          this.finalCamera = new Camera(finalCamera.getX(), finalCamera.getY(), finalCamera.getZ(),
              finalCamera.getYaw(), finalCamera.getPitch(), finalCamera.getFieldOfView());
         
          // Create an animation that will interpolate camera location
          // between initial camera and final camera in 150 ms
          if (alpha == null) {
            alpha = new Alpha(1, 150);
            setAlpha(alpha);
          }
          // Start animation now
          alpha.setStartTime(System.currentTimeMillis());
          // In case system is overloaded computeTransform won't be called
          // ensure final location will always be set after 150 ms
          this.scheduledExecutor.schedule(new Runnable() {
              public void run() {
                if (getAlpha().value() == 1) {
                  Transform3D transform = new Transform3D();
                  computeTransform(1, transform);
                  getTarget().setTransform(transform);
                }
              }
            }, 150, TimeUnit.MILLISECONDS);
        }
      }
    }
   
    @Override
    public synchronized void computeTransform(float alpha, Transform3D transform) {
      updateViewPlatformTransform(transform,
          this.initialCamera.getX() + (this.finalCamera.getX() - this.initialCamera.getX()) * alpha,
          this.initialCamera.getY() + (this.finalCamera.getY() - this.initialCamera.getY()) * alpha,
          this.initialCamera.getZ() + (this.finalCamera.getZ() - this.initialCamera.getZ()) * alpha,
          this.initialCamera.getYaw() + (this.finalCamera.getYaw() - this.initialCamera.getYaw()) * alpha,
          this.initialCamera.getPitch() + (this.finalCamera.getPitch() - this.initialCamera.getPitch()) * alpha);
    }
  }
 
  /**
   * Updates <code>viewPlatformTransform</code> transform from camera angles and location.
   */
  private void updateViewPlatformTransform(Transform3D transform,
                                           float cameraX, float cameraY, float cameraZ,
                                           float cameraYaw, float cameraPitch) {
    Transform3D yawRotation = new Transform3D();
    yawRotation.rotY(-cameraYaw + Math.PI);
   
    Transform3D pitchRotation = new Transform3D();
    pitchRotation.rotX(-cameraPitch);
    yawRotation.mul(pitchRotation);
   
    transform.setIdentity();
    transform.setTranslation(new Vector3f(cameraX, cameraZ, cameraY));
    transform.mul(yawRotation);
   
    this.camera = new Camera(cameraX, cameraY, cameraZ, cameraYaw, cameraPitch, 0);
  }

  /**
   * Adds AWT mouse listeners to <code>component3D</code> that calls back <code>controller</code> methods. 
   */
  private void addMouseListeners(final HomeController3D controller, final Component component3D) {
    MouseInputAdapter mouseListener = new MouseInputAdapter() {
        private int        xLastMouseMove;
        private int        yLastMouseMove;
        private Component  grabComponent;
        private Component  previousMouseEventTarget;
       
        @Override
        public void mousePressed(MouseEvent ev) {
          if (!retargetMouseEventToNavigationPanelChildren(ev)) {
            if (ev.isPopupTrigger()) {
              mouseReleased(ev);
            } else if (isEnabled()) {
              requestFocusInWindow();
              this.xLastMouseMove = ev.getX();
              this.yLastMouseMove = ev.getY();
            }
          }
        }
 
        @Override
        public void mouseReleased(MouseEvent ev) {
          if (!retargetMouseEventToNavigationPanelChildren(ev)) {
            if (ev.isPopupTrigger()) {
              JPopupMenu componentPopupMenu = getComponentPopupMenu();
              if (componentPopupMenu != null) {
                componentPopupMenu.show(HomeComponent3D.this, ev.getX(), ev.getY());
              }
            }
          }
        }

        @Override
        public void mouseClicked(MouseEvent ev) {
          retargetMouseEventToNavigationPanelChildren(ev);
        }
       
        @Override
        public void mouseMoved(MouseEvent ev) {
          retargetMouseEventToNavigationPanelChildren(ev);
        }
       
        @Override
        public void mouseDragged(MouseEvent ev) {
          if (!retargetMouseEventToNavigationPanelChildren(ev)) {
            if (isEnabled()) {
              if (ev.isAltDown()) {
                // Mouse move along Y axis while alt is down changes camera location
                float delta = 1.25f * (this.yLastMouseMove - ev.getY());
                // Multiply delta by 10 if shift is down
                if (ev.isShiftDown()) {
                  delta *= 5;
                }
                controller.moveCamera(delta);
              } else {
                final float ANGLE_FACTOR = 0.005f;
                // Mouse move along X axis changes camera yaw
                float yawDelta = ANGLE_FACTOR * (ev.getX() - this.xLastMouseMove);
                // Multiply yaw delta by 10 if shift is down
                if (ev.isShiftDown()) {
                  yawDelta *= 5;
                }
                controller.rotateCameraYaw(yawDelta);
               
                // Mouse move along Y axis changes camera pitch
                float pitchDelta = ANGLE_FACTOR * (ev.getY() - this.yLastMouseMove);
                controller.rotateCameraPitch(pitchDelta);
              }
             
              this.xLastMouseMove = ev.getX();
              this.yLastMouseMove = ev.getY();
            }
          }
        }
       
        /**
         * Retargets to the first component of navigation panel able to manage the given event
         * and returns <code>true</code> if a component consumed the event
         * or needs to be repainted (meaning its state changed).
         * This implementation doesn't cover all the possible cases (mouseEntered and mouseExited
         * events are managed only during mouseDragged event).
         */
        private boolean retargetMouseEventToNavigationPanelChildren(MouseEvent ev) {
          if (navigationPanel != null
              && navigationPanel.isVisible()) {
            if (this.grabComponent != null
                && (ev.getID() == MouseEvent.MOUSE_RELEASED
                    || ev.getID() == MouseEvent.MOUSE_DRAGGED)) {
              Point point = SwingUtilities.convertPoint(ev.getComponent(), ev.getPoint(), this.grabComponent);
              dispatchRetargetedEvent(deriveEvent(ev, this.grabComponent, ev.getID(), point.x, point.y));
              if (ev.getID() == MouseEvent.MOUSE_RELEASED) {
                this.grabComponent = null;
              } else {
                if (this.previousMouseEventTarget == null
                    && this.grabComponent.contains(point)) {
                  dispatchRetargetedEvent(deriveEvent(ev, this.grabComponent, MouseEvent.MOUSE_ENTERED, point.x, point.y));
                  this.previousMouseEventTarget = this.grabComponent;
                } else if (this.previousMouseEventTarget != null
                    && !this.grabComponent.contains(point)) {
                  dispatchRetargetedEvent(deriveEvent(ev, this.grabComponent, MouseEvent.MOUSE_EXITED, point.x, point.y));
                  this.previousMouseEventTarget = null;
                }
              }
              return true;
            } else {               
              Component mouseEventTarget = retargetMouseEvent(navigationPanel, ev);
              if (mouseEventTarget != null) {
                this.previousMouseEventTarget = mouseEventTarget;
                return true;
              }
            }
          }
          return false;
        }
       
        private Component retargetMouseEvent(Component component, MouseEvent ev) {
          if (component.getBounds().contains(ev.getPoint())) {
            if (component instanceof Container) {
              Container container = (Container)component;
              for (int i = container.getComponentCount() - 1; i >= 0; i--) {
                Component c = container.getComponent(i);
                MouseEvent retargetedEvent = deriveEvent(ev, component, ev.getID(),
                    ev.getX() - component.getX(), ev.getY() - component.getY());
                Component mouseEventTarget = retargetMouseEvent(c, retargetedEvent);
                if (mouseEventTarget != null) {
                  return mouseEventTarget;
                }
              }
            }
            int newX = ev.getX() - component.getX();
            int newY = ev.getY() - component.getY();
            if (dispatchRetargetedEvent(deriveEvent(ev, component, ev.getID(), newX, newY))) {             
              if (ev.getID() == MouseEvent.MOUSE_PRESSED) {
                this.grabComponent = component;
             
              return component;
            }
          }
          return null;
        }
       
        /**
         * Dispatches the given event to its component and returns <code>true</code> if component needs to be redrawn.
         */
        private boolean dispatchRetargetedEvent(MouseEvent ev) {
          ev.getComponent().dispatchEvent(ev);
          if (!RepaintManager.currentManager(ev.getComponent()).getDirtyRegion((JComponent)ev.getComponent()).isEmpty()) {
            updateNavigationPanelImage();
            component3D.repaint();
            return true;
          }
          return false;
        }
       
        /**
         * Returns a new <code>MouseEvent</code> derived from the one given in parameter.
         */
        private MouseEvent deriveEvent(MouseEvent ev, Component component, int id, int x, int y) {
          return new MouseEvent(component, id, ev.getWhen(),
              ev.getModifiersEx() | ev.getModifiers(), x, y,
              ev.getClickCount(), ev.isPopupTrigger(), ev.getButton());
        }
      };
    MouseWheelListener mouseWheelListener = new MouseWheelListener() {
        public void mouseWheelMoved(MouseWheelEvent ev) {
          if (isEnabled()) {
            // Mouse wheel changes camera location
            float delta = -2.5f * ev.getWheelRotation();
            // Multiply delta by 10 if shift is down
            if (ev.isShiftDown()) {
              delta *= 5;
            }
            controller.moveCamera(delta);
          }
        }
      };
   
    component3D.addMouseListener(mouseListener);
    component3D.addMouseMotionListener(mouseListener);
    component3D.addMouseWheelListener(mouseWheelListener);
    // Add a mouse listener to this component to request focus in case user clicks in component border
    this.addMouseListener(new MouseInputAdapter() {
        @Override
        public void mousePressed(MouseEvent e) {
          requestFocusInWindow();
        }
      });
  }

  /**
   * Installs keys bound to actions.
   */
  private void installKeyboardActions() {
    InputMap inputMap = getInputMap(WHEN_FOCUSED);
    inputMap.put(KeyStroke.getKeyStroke("shift UP"), ActionType.MOVE_CAMERA_FAST_FORWARD);
    inputMap.put(KeyStroke.getKeyStroke("shift W"), ActionType.MOVE_CAMERA_FAST_FORWARD);
    inputMap.put(KeyStroke.getKeyStroke("UP"), ActionType.MOVE_CAMERA_FORWARD);
    inputMap.put(KeyStroke.getKeyStroke("W"), ActionType.MOVE_CAMERA_FORWARD);
    inputMap.put(KeyStroke.getKeyStroke("shift DOWN"), ActionType.MOVE_CAMERA_FAST_BACKWARD);
    inputMap.put(KeyStroke.getKeyStroke("shift S"), ActionType.MOVE_CAMERA_FAST_BACKWARD);
    inputMap.put(KeyStroke.getKeyStroke("DOWN"), ActionType.MOVE_CAMERA_BACKWARD);
    inputMap.put(KeyStroke.getKeyStroke("S"), ActionType.MOVE_CAMERA_BACKWARD);
    inputMap.put(KeyStroke.getKeyStroke("shift LEFT"), ActionType.ROTATE_CAMERA_YAW_FAST_LEFT);
    inputMap.put(KeyStroke.getKeyStroke("shift A"), ActionType.ROTATE_CAMERA_YAW_FAST_LEFT);
    inputMap.put(KeyStroke.getKeyStroke("LEFT"), ActionType.ROTATE_CAMERA_YAW_LEFT);
    inputMap.put(KeyStroke.getKeyStroke("A"), ActionType.ROTATE_CAMERA_YAW_LEFT);
    inputMap.put(KeyStroke.getKeyStroke("shift RIGHT"), ActionType.ROTATE_CAMERA_YAW_FAST_RIGHT);
    inputMap.put(KeyStroke.getKeyStroke("shift D"), ActionType.ROTATE_CAMERA_YAW_FAST_RIGHT);
    inputMap.put(KeyStroke.getKeyStroke("RIGHT"), ActionType.ROTATE_CAMERA_YAW_RIGHT);
    inputMap.put(KeyStroke.getKeyStroke("D"), ActionType.ROTATE_CAMERA_YAW_RIGHT);
    inputMap.put(KeyStroke.getKeyStroke("PAGE_UP"), ActionType.ROTATE_CAMERA_PITCH_UP);
    inputMap.put(KeyStroke.getKeyStroke("PAGE_DOWN"), ActionType.ROTATE_CAMERA_PITCH_DOWN);
    inputMap.put(KeyStroke.getKeyStroke("shift HOME"), ActionType.ELEVATE_CAMERA_FAST_UP);
    inputMap.put(KeyStroke.getKeyStroke("HOME"), ActionType.ELEVATE_CAMERA_UP);
    inputMap.put(KeyStroke.getKeyStroke("shift END"), ActionType.ELEVATE_CAMERA_FAST_DOWN);
    inputMap.put(KeyStroke.getKeyStroke("END"), ActionType.ELEVATE_CAMERA_DOWN);
  }
  /**
   * Creates actions that calls back <code>controller</code> methods. 
   */
  private void createActions(final HomeController3D controller) {
    // Move camera action mapped to arrow keys
    class MoveCameraAction extends AbstractAction {
      private final float delta;
     
      public MoveCameraAction(float delta) {
        this.delta = delta;
      }

      public void actionPerformed(ActionEvent e) {
        controller.moveCamera(this.delta);
      }
    }
    // Elevate camera action mapped to arrow keys
    class ElevateCameraAction extends AbstractAction {
      private final float delta;
     
      public ElevateCameraAction(float delta) {
        this.delta = delta;
      }

      public void actionPerformed(ActionEvent e) {
        controller.elevateCamera(this.delta);
      }
    }
    // Rotate camera yaw action mapped to arrow keys
    class RotateCameraYawAction extends AbstractAction {
      private final float delta;
     
      public RotateCameraYawAction(float delta) {
        this.delta = delta;
      }

      public void actionPerformed(ActionEvent e) {
        controller.rotateCameraYaw(this.delta);
      }
    }
    // Rotate camera pitch action mapped to arrow keys
    class RotateCameraPitchAction extends AbstractAction {
      private final float delta;
     
      public RotateCameraPitchAction(float delta) {
        this.delta = delta;
      }

      public void actionPerformed(ActionEvent e) {
        controller.rotateCameraPitch(this.delta);
      }
    }
    ActionMap actionMap = getActionMap();
    actionMap.put(ActionType.MOVE_CAMERA_FORWARD, new MoveCameraAction(6.5f));
    actionMap.put(ActionType.MOVE_CAMERA_FAST_FORWARD, new MoveCameraAction(32.5f));
    actionMap.put(ActionType.MOVE_CAMERA_BACKWARD, new MoveCameraAction(-6.5f));
    actionMap.put(ActionType.MOVE_CAMERA_FAST_BACKWARD, new MoveCameraAction(-32.5f));
    actionMap.put(ActionType.ELEVATE_CAMERA_DOWN, new ElevateCameraAction(-2.5f));
    actionMap.put(ActionType.ELEVATE_CAMERA_FAST_DOWN, new ElevateCameraAction(-10f));
    actionMap.put(ActionType.ELEVATE_CAMERA_UP, new ElevateCameraAction(2.5f));
    actionMap.put(ActionType.ELEVATE_CAMERA_FAST_UP, new ElevateCameraAction(10f));
    actionMap.put(ActionType.ROTATE_CAMERA_YAW_LEFT, new RotateCameraYawAction(-(float)Math.PI / 60));
    actionMap.put(ActionType.ROTATE_CAMERA_YAW_FAST_LEFT, new RotateCameraYawAction(-(float)Math.PI / 12));
    actionMap.put(ActionType.ROTATE_CAMERA_YAW_RIGHT, new RotateCameraYawAction((float)Math.PI / 60));
    actionMap.put(ActionType.ROTATE_CAMERA_YAW_FAST_RIGHT, new RotateCameraYawAction((float)Math.PI / 12));
    actionMap.put(ActionType.ROTATE_CAMERA_PITCH_UP, new RotateCameraPitchAction(-(float)Math.PI / 120));
    actionMap.put(ActionType.ROTATE_CAMERA_PITCH_DOWN, new RotateCameraPitchAction((float)Math.PI / 120));
  }

  /**
   * Returns a new scene tree root.
   */
  private BranchGroup createSceneTree(boolean displayShadowOnFloor,
                                      boolean listenToHomeUpdates,
                                      boolean waitForLoading) {
    BranchGroup root = new BranchGroup();
    // Build scene tree
    root.addChild(createHomeTree(displayShadowOnFloor, listenToHomeUpdates, waitForLoading));
    root.addChild(createBackgroundNode(listenToHomeUpdates, waitForLoading));
    root.addChild(createGroundNode(-1E5f / 2, -1E5f / 2, 1E5f, 1E5f, listenToHomeUpdates, waitForLoading));

    // Add default lights
    for (Light light : createLights(listenToHomeUpdates)) {
      root.addChild(light);
    }
   
    return root;
  }

  /**
   * Returns a new background node. 
   */
  private Node createBackgroundNode(boolean listenToHomeUpdates, final boolean waitForLoading) {
    final Appearance backgroundAppearance = new Appearance();
    ColoringAttributes backgroundColoringAttributes = new ColoringAttributes();
    backgroundAppearance.setColoringAttributes(backgroundColoringAttributes);
    // Allow background color and texture to change
    backgroundAppearance.setCapability(Appearance.ALLOW_TEXTURE_WRITE);
    backgroundAppearance.setCapability(Appearance.ALLOW_COLORING_ATTRIBUTES_READ);
    backgroundColoringAttributes.setCapability(ColoringAttributes.ALLOW_COLOR_WRITE);
   
    Geometry halfSphereGeometry = createHalfSphereGeometry();  
    final Shape3D halfSphere = new Shape3D(halfSphereGeometry, backgroundAppearance);
    BranchGroup backgroundBranch = new BranchGroup();
    backgroundBranch.addChild(halfSphere);
   
    final Background background = new Background(backgroundBranch);
    updateBackgroundColorAndTexture(backgroundAppearance, this.home, waitForLoading);
    background.setImageScaleMode(Background.SCALE_FIT_ALL);
    background.setApplicationBounds(new BoundingBox(
        new Point3d(-1E6, -1E6, -1E6),
        new Point3d(1E6, 1E6, 1E6)));   
   
    if (listenToHomeUpdates) {
      // Add a listener on sky color and texture properties change
      this.skyColorListener = new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            updateBackgroundColorAndTexture(backgroundAppearance, home, waitForLoading);
          }
        };
      this.home.getEnvironment().addPropertyChangeListener(
          HomeEnvironment.Property.SKY_COLOR, this.skyColorListener);
      this.home.getEnvironment().addPropertyChangeListener(
          HomeEnvironment.Property.SKY_TEXTURE, this.skyColorListener);
    }
    return background;
  }

  /**
   * Returns a half sphere oriented inward and with texture ordinates
   * that spread along an hemisphere.
   */
  private Geometry createHalfSphereGeometry() {
    final int divisionCount = 48;
    Point3f [] coords = new Point3f [divisionCount * divisionCount];
    TexCoord2f [] textureCoords = new TexCoord2f [divisionCount * divisionCount];
    for (int i = 0, k = 0; i < divisionCount; i++) {
      double alpha = i * 2 * Math.PI / divisionCount;
      float cosAlpha = (float)Math.cos(alpha);
      float sinAlpha = (float)Math.sin(alpha);
      double nextAlpha = (i  + 1) * 2 * Math.PI / divisionCount;
      float cosNextAlpha = (float)Math.cos(nextAlpha);
      float sinNextAlpha = (float)Math.sin(nextAlpha);
      for (int j = 0; j < divisionCount / 4; j++) {
        double beta = 2 * j * Math.PI / divisionCount;
        float cosBeta = (float)Math.cos(beta);
        float sinBeta = (float)Math.sin(beta);
        // Correct the bottom of the hemisphere to avoid seeing a black line at the horizon
        float y = j != 0 ? sinBeta : -0.05f;
        double nextBeta = 2 * (j + 1) * Math.PI / divisionCount;
        float cosNextBeta = (float)Math.cos(nextBeta);
        float sinNextBeta = (float)Math.sin(nextBeta);
        coords [k] = new Point3f(cosAlpha * cosBeta, y, sinAlpha * cosBeta);
        textureCoords [k++] = new TexCoord2f((float)i / divisionCount, sinBeta);
       
        coords [k] = new Point3f(cosNextAlpha * cosBeta, y, sinNextAlpha * cosBeta);
        textureCoords [k++] = new TexCoord2f((float)(i + 1) / divisionCount, sinBeta);
       
        coords [k] = new Point3f(cosNextAlpha * cosNextBeta, sinNextBeta, sinNextAlpha * cosNextBeta);
        textureCoords [k++] = new TexCoord2f((float)(i + 1) / divisionCount, sinNextBeta);
       
        coords [k] = new Point3f(cosAlpha * cosNextBeta, sinNextBeta, sinAlpha * cosNextBeta);
        textureCoords [k++] = new TexCoord2f((float)i / divisionCount, sinNextBeta);
      }
    }
   
    GeometryInfo geometryInfo = new GeometryInfo(GeometryInfo.QUAD_ARRAY);
    geometryInfo.setCoordinates(coords);
    geometryInfo.setTextureCoordinateParams(1, 2);
    geometryInfo.setTextureCoordinates(0, textureCoords);
    geometryInfo.indexify();
    geometryInfo.compact();
    Geometry halfSphereGeometry = geometryInfo.getIndexedGeometryArray();
    return halfSphereGeometry;
  }

  /**
   * Updates<code>backgroundAppearance</code> color and texture from <code>home</code> sky color and texture.
   */
  private void updateBackgroundColorAndTexture(final Appearance backgroundAppearance, Home home,
                                               boolean waitForLoading) {
    Color3f skyColor = new Color3f(new Color(home.getEnvironment().getSkyColor()));
    backgroundAppearance.getColoringAttributes().setColor(skyColor);
    HomeTexture skyTexture = home.getEnvironment().getSkyTexture();
    if (skyTexture != null) {
      TextureManager textureManager = TextureManager.getInstance();
      if (waitForLoading) {
        // Don't share the background texture otherwise if might not be rendered correctly
        backgroundAppearance.setTexture(textureManager.loadTexture(skyTexture.getImage()));
      } else {
        textureManager.loadTexture(skyTexture.getImage(), waitForLoading,
            new TextureManager.TextureObserver() {
                public void textureUpdated(Texture texture) {
                  backgroundAppearance.setTexture(texture);
                }
              });
      }
    } else {
      backgroundAppearance.setTexture(null);
    }

    clearPrintedImageCache();
  }
 
  /**
   * Returns a new ground node. 
   */
  private Node createGroundNode(final float groundOriginX,
                                final float groundOriginY,
                                final float groundWidth,
                                final float groundDepth,
                                boolean listenToHomeUpdates,
                                boolean waitForLoading) {
    final Ground3D ground3D = new Ground3D(this.home,
        groundOriginX, groundOriginY, groundWidth, groundDepth, waitForLoading);
    Transform3D translation = new Transform3D();
    translation.setTranslation(new Vector3f(0, -0.2f, 0));
    TransformGroup transformGroup = new TransformGroup(translation);
    transformGroup.addChild(ground3D);
   
    if (listenToHomeUpdates) {
      // Add a listener on ground color and texture properties change
      this.groundColorAndTextureListener = new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            ground3D.update();
            clearPrintedImageCache();
          }
        };
      HomeEnvironment homeEnvironment = this.home.getEnvironment();
      homeEnvironment.addPropertyChangeListener(
          HomeEnvironment.Property.GROUND_COLOR, this.groundColorAndTextureListener);
      homeEnvironment.addPropertyChangeListener(
          HomeEnvironment.Property.GROUND_TEXTURE, this.groundColorAndTextureListener);
    }
   
    return transformGroup;
  }
 
  /**
   * Returns the lights of the scene.
   */
  private Light [] createLights(boolean listenToHomeUpdates) {
    final Light [] lights = {
        new DirectionalLight(new Color3f(1, 1, 1), new Vector3f(1.5f, -0.8f, -1)),        
        new DirectionalLight(new Color3f(1, 1, 1), new Vector3f(-1.5f, -0.8f, -1)),
        new DirectionalLight(new Color3f(1, 1, 1), new Vector3f(0, -0.8f, 1)),
        new DirectionalLight(new Color3f(0.7f, 0.7f, 0.7f), new Vector3f(0, 1f, 0)),
        new AmbientLight(new Color3f(0.2f, 0.2f, 0.2f))};
    for (int i = 0; i < lights.length - 1; i++) {
      // Allow directional lights color to change
      lights [i].setCapability(DirectionalLight.ALLOW_COLOR_WRITE);
      // Store default color in user data
      Color3f defaultColor = new Color3f();
      lights [i].getColor(defaultColor);
      lights [i].setUserData(defaultColor);
      updateLightColor(lights [i]);
    }
   
    for (Light light : lights) {
      light.setInfluencingBounds(new BoundingSphere(new Point3d(), 1E7));
    }
   
    if (listenToHomeUpdates) {
      // Add a listener on light color property change to home
      this.lightColorListener = new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            for (int i = 0; i < lights.length - 1; i++) {
              updateLightColor(lights [i]);
            }
          }
        };
      this.home.getEnvironment().addPropertyChangeListener(
          HomeEnvironment.Property.LIGHT_COLOR, this.lightColorListener);
    }
   
    return lights;
  }

  /**
   * Updates<code>light</code> color from <code>home</code> light color.
   */
  private void updateLightColor(Light light) {
    Color3f defaultColor = (Color3f)light.getUserData();
    int lightColor = this.home.getEnvironment().getLightColor();
    light.setColor(new Color3f(((lightColor >>> 16) & 0xFF) / 255f * defaultColor.x,
                                ((lightColor >>> 8) & 0xFF) / 255f * defaultColor.y,
                                        (lightColor & 0xFF) / 255f * defaultColor.z));
    clearPrintedImageCache();
  }

  /**
   * Returns a <code>home</code> new tree node, with branches for each wall
   * and piece of furniture of <code>home</code>.
   */
  private Node createHomeTree(boolean displayShadowOnFloor,
                              boolean listenToHomeUpdates,
                              boolean waitForLoading) {
    Group homeRoot = createHomeRoot();
    // Add walls, pieces and rooms already available
    for (Room room : this.home.getRooms()) {
      addObject(homeRoot, room, listenToHomeUpdates, waitForLoading);
    }   
    for (Wall wall : this.home.getWalls()) {
      addObject(homeRoot, wall, listenToHomeUpdates, waitForLoading);
    }
    Map<HomePieceOfFurniture, Node> pieces3D = new HashMap<HomePieceOfFurniture, Node>();
    for (HomePieceOfFurniture piece : this.home.getFurniture()) {
      pieces3D.put(piece, addObject(homeRoot, piece, listenToHomeUpdates, waitForLoading));
    }
   
    if (displayShadowOnFloor) {
      addShadowOnFloor(homeRoot, pieces3D);
    }
   
    if (listenToHomeUpdates) {
      // Add wall, furniture, room listeners to home for further update   
      addWallListener(homeRoot);
      addFurnitureListener(homeRoot);
      addRoomListener(homeRoot);
      // Add environment listeners
      addEnvironmentListeners();
      // Should update shadow on floor too but in the facts
      // User Interface doesn't propose to modify the furniture of a home
      // that displays shadow on floor yet
    }
    return homeRoot;
  }

  /**
   * Returns a new group at home subtree root.
   */
  private Group createHomeRoot() {
    Group homeGroup = new Group();   
    //  Allow group to have new children
    homeGroup.setCapability(Group.ALLOW_CHILDREN_WRITE);
    homeGroup.setCapability(Group.ALLOW_CHILDREN_EXTEND);
    return homeGroup;
  }

  /**
   * Adds a wall listener to home walls that updates the children of the given
   * <code>group</code>, each time a wall is added, updated or deleted.
   */
  private void addWallListener(final Group group) {
    this.wallChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          updateWall((Wall)ev.getSource());         
          updateObjects(home.getRooms());
        }
      };
    for (Wall wall : this.home.getWalls()) {
      wall.addPropertyChangeListener(this.wallChangeListener);
    }     
    this.wallListener = new CollectionListener<Wall>() {
        public void collectionChanged(CollectionEvent<Wall> ev) {
          Wall wall = ev.getItem();
          switch (ev.getType()) {
            case ADD :
              addObject(group, wall, true, false);
              wall.addPropertyChangeListener(wallChangeListener);
              break;
            case DELETE :
              deleteObject(wall);
              wall.removePropertyChangeListener(wallChangeListener);
              break;
          }
          updateObjects(home.getRooms());
        }
      };
    this.home.addWallsListener(this.wallListener);
  }

  /**
   * Adds a furniture listener to home that updates the children of the given <code>group</code>,
   * each time a piece of furniture is added, updated or deleted.
   */
  private void addFurnitureListener(final Group group) {
    this.furnitureChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
        if (HomePieceOfFurniture.Property.X.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.X.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.Y.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.ANGLE.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.WIDTH.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.DEPTH.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.HEIGHT.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.ELEVATION.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.COLOR.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.TEXTURE.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.SHININESS.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.MODEL_MIRRORED.name().equals(ev.getPropertyName())
            || HomePieceOfFurniture.Property.VISIBLE.name().equals(ev.getPropertyName())) {
              HomePieceOfFurniture piece = (HomePieceOfFurniture)ev.getSource();
              updateObjects(Arrays.asList(new HomePieceOfFurniture [] {piece}));
              // If piece is or contains a door or a window, update walls that intersect with piece
              if (containsDoorsAndWindows(piece)) {
                updateObjects(home.getWalls());
              }
          }
        }
      };
    for (HomePieceOfFurniture piece : this.home.getFurniture()) {
      piece.addPropertyChangeListener(this.furnitureChangeListener);
    }     
    this.furnitureListener = new CollectionListener<HomePieceOfFurniture>() {
        public void collectionChanged(CollectionEvent<HomePieceOfFurniture> ev) {
          HomePieceOfFurniture piece = (HomePieceOfFurniture)ev.getItem();
          switch (ev.getType()) {
            case ADD :
              addObject(group, piece, true, false);
              piece.addPropertyChangeListener(furnitureChangeListener);
              break;
            case DELETE :
              deleteObject(piece);
              piece.removePropertyChangeListener(furnitureChangeListener);
              break;
          }
          // If piece is or contains a door or a window, update walls that intersect with piece
          if (containsDoorsAndWindows(piece)) {
            updateObjects(home.getWalls());
          }
        }
      };
    this.home.addFurnitureListener(this.furnitureListener);
  }

  /**
   * Returns <code>true</code> if the given <code>piece</code> is or contains a door or window. 
   */
  private boolean containsDoorsAndWindows(HomePieceOfFurniture piece) {
    if (piece instanceof HomeFurnitureGroup) {
      for (HomePieceOfFurniture groupPiece : ((HomeFurnitureGroup)piece).getFurniture()) {
        if (containsDoorsAndWindows(groupPiece)) {
          return true;
        }
      }
      return false;
    } else {
      return piece.isDoorOrWindow();
    }
  }
 
  /**
   * Adds a room listener to home rooms that updates the children of the given
   * <code>group</code>, each time a room is added, updated or deleted.
   */
  private void addRoomListener(final Group group) {
    this.roomChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          if (Room.Property.FLOOR_COLOR.name().equals(ev.getPropertyName())
              || Room.Property.FLOOR_TEXTURE.name().equals(ev.getPropertyName())
              || Room.Property.FLOOR_SHININESS.name().equals(ev.getPropertyName())
              || Room.Property.CEILING_COLOR.name().equals(ev.getPropertyName())
              || Room.Property.CEILING_TEXTURE.name().equals(ev.getPropertyName())
              || Room.Property.CEILING_SHININESS.name().equals(ev.getPropertyName())) {
            updateObjects(Arrays.asList(new Room [] {(Room)ev.getSource()}));
          } else if (Room.Property.FLOOR_VISIBLE.name().equals(ev.getPropertyName())
              || Room.Property.CEILING_VISIBLE.name().equals(ev.getPropertyName())) {  
            updateObjects(home.getRooms());
            groundColorAndTextureListener.propertyChange(null);
          } else if (Room.Property.POINTS.name().equals(ev.getPropertyName())) {  
            if (homeObjectsToUpdate != null) {
              // Don't try to optimize if more than one room to update
              updateObjects(home.getRooms());
            } else {
              updateObjects(Arrays.asList(new Room [] {(Room)ev.getSource()}));
              // Search the rooms that overlap the updated one
              Area oldArea = getArea((float [][])ev.getOldValue());
              Area newArea = getArea((float [][])ev.getNewValue());
              for (Room room : home.getRooms()) {
                if (room != ev.getSource()) {
                  Area roomAreaIntersectionWithOldArea = getArea(room.getPoints());
                  Area roomAreaIntersectionWithNewArea = new Area(roomAreaIntersectionWithOldArea);
                  roomAreaIntersectionWithNewArea.intersect(newArea);                 
                  if (!roomAreaIntersectionWithNewArea.isEmpty()) {
                    updateObjects(Arrays.asList(new Room [] {room}));
                  } else {
                    roomAreaIntersectionWithOldArea.intersect(oldArea);
                    if (!roomAreaIntersectionWithOldArea.isEmpty()) {
                      updateObjects(Arrays.asList(new Room [] {room}));
                    }
                  }
                }
              }             
            }
            groundColorAndTextureListener.propertyChange(null);
          }           
        }
       
        private Area getArea(float [][] points) {
          GeneralPath path = new GeneralPath();
          path.moveTo(points [0][0], points [0][1]);
          for (int i = 1; i < points.length; i++) {
            path.lineTo(points [i][0], points [i][1]);
          }
          path.closePath();
          return new Area(path);
        }
      };
    for (Room room : this.home.getRooms()) {
      room.addPropertyChangeListener(this.roomChangeListener);
    }     
    this.roomListener = new CollectionListener<Room>() {
        public void collectionChanged(CollectionEvent<Room> ev) {
          Room room = ev.getItem();
          switch (ev.getType()) {
            case ADD :
              // Add room to its group at the index indicated by the event
              // to ensure the 3D rooms are drawn in the same order as in the plan 
              addObject(group, room, ev.getIndex(), true, false);
              room.addPropertyChangeListener(roomChangeListener);
              break;
            case DELETE :
              deleteObject(room);
              room.removePropertyChangeListener(roomChangeListener);
              break;
          }
          updateObjects(home.getRooms());
          groundColorAndTextureListener.propertyChange(null);
        }
      };
    this.home.addRoomsListener(this.roomListener);
  }

  /**
   * Adds a walls alpha change listener and drawing mode change listener to home
   * environment that updates the home scene objects appearance.
   */
  private void addEnvironmentListeners() {
    this.wallsAlphaListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          updateObjects(home.getWalls());
        }
      };
    this.home.getEnvironment().addPropertyChangeListener(
        HomeEnvironment.Property.WALLS_ALPHA, this.wallsAlphaListener);
    this.drawingModeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          updateObjects(home.getWalls());
          updateObjects(home.getFurniture());
        }
      };
    this.home.getEnvironment().addPropertyChangeListener(
        HomeEnvironment.Property.DRAWING_MODE, this.drawingModeListener);
  }

  /**
   * Adds to <code>group</code> a branch matching <code>homeObject</code>.
   */
  private Node addObject(Group group, Selectable homeObject, boolean listenToHomeUpdates, boolean waitForLoading) {
    return addObject(group, homeObject, -1, listenToHomeUpdates, waitForLoading);
  }

  /**
   * Adds to <code>group</code> a branch matching <code>homeObject</code> at a given <code>index</code>.
   * If <code>index</code> is equal to -1, <code>homeObject</code> will be added at the end of the group.
   */
  private Node addObject(Group group, Selectable homeObject, int index,
                         boolean listenToHomeUpdates, boolean waitForLoading) {
    Object3DBranch object3D = createObject3D(homeObject, waitForLoading);
    if (listenToHomeUpdates) {
      this.homeObjects.put(homeObject, object3D);
    }
    if (index == -1) {
      group.addChild(object3D);
    } else {
      group.insertChild(object3D, index);
    }
    clearPrintedImageCache();
    return object3D;
  }

  /**
   * Returns the 3D object matching the given home object. If <code>waitForLoading</code>
   * is <code>true</code> the resources used by the returned 3D object should be ready to be displayed.
   * @deprecated Subclasses which used to override this method must be updated to create an instance of 
   *    a {@link Object3DFactory factory} and give it as parameter to the constructor of this class.
   */
  private Object3DBranch createObject3D(Selectable homeObject,
                                        boolean waitForLoading) {
    return (Object3DBranch)this.object3dFactory.createObject3D(this.home, homeObject, waitForLoading);
  }

  /**
   * Detaches from the scene the branch matching <code>homeObject</code>.
   */
  private void deleteObject(Selectable homeObject) {
    this.homeObjects.get(homeObject).detach();
    this.homeObjects.remove(homeObject);
    clearPrintedImageCache();
  }

  /**
   * Updates <code>objects</code> later. Should be invoked from Event Dispatch Thread.
   */
  private void updateObjects(Collection<? extends Selectable> objects) {
    if (this.homeObjectsToUpdate != null) {
      this.homeObjectsToUpdate.addAll(objects);
    } else {
      this.homeObjectsToUpdate = new HashSet<Selectable>(objects);
      // Invoke later the update of objects of homeObjectsToUpdate
      EventQueue.invokeLater(new Runnable () {
        public void run() {
          for (Selectable object : homeObjectsToUpdate) {
            Object3DBranch objectBranch = homeObjects.get(object);
            // Check object wasn't deleted since updateObjects call
            if (objectBranch != null) {
              homeObjects.get(object).update();
            }
          }
          homeObjectsToUpdate = null;
        }
      });
    }
    clearPrintedImageCache();
  }
 
  /**
   * Updates <code>wall</code> geometry,
   * and the walls at its end or start.
   */
  private void updateWall(Wall wall) {
    Collection<Wall> wallsToUpdate = new ArrayList<Wall>(3);
    wallsToUpdate.add(wall);
    if (wall.getWallAtStart() != null) {
      wallsToUpdate.add(wall.getWallAtStart());               
    }
    if (wall.getWallAtEnd() != null) {
      wallsToUpdate.add(wall.getWallAtEnd());               
    }
    updateObjects(wallsToUpdate);
  }

  /**
   * Adds to <code>homeRoot</code> a shape matching the shadow of furniture at ground level.
   */
  private void addShadowOnFloor(Group homeRoot, Map<HomePieceOfFurniture, Node> pieces3D) {
    Area areaOnFloor = new Area();
    // Compute union of the areas of pieces at ground level that are not lights, doors or windows
    for (Map.Entry<HomePieceOfFurniture, Node> object3DEntry : pieces3D.entrySet()) {
      if (object3DEntry.getKey() instanceof HomePieceOfFurniture) {
        HomePieceOfFurniture piece = object3DEntry.getKey();
        // This operation can be lengthy, so give up if thread is interrupted
        if (Thread.currentThread().isInterrupted()) {
          return;
        }
        if (piece.getElevation() == 0
            && !piece.isDoorOrWindow()
            && !(piece instanceof com.eteks.sweethome3d.model.Light)) {
          Area pieceAreaOnFloor = ModelManager.getInstance().getAreaOnFloor(object3DEntry.getValue());
          areaOnFloor.add(pieceAreaOnFloor);
        }
      }
    }
   
    // Create the 3D shape matching computed area
    List<Point3f> coords = new ArrayList<Point3f>();
    List<Integer> stripCounts = new ArrayList<Integer>();
    int pointsCount = 0;
    float [] modelPoint = new float[2];
    for (PathIterator it = areaOnFloor.getPathIterator(null); !it.isDone(); ) {
      if (it.currentSegment(modelPoint) == PathIterator.SEG_CLOSE) {
        stripCounts.add(pointsCount);
        pointsCount = 0;
      } else {
        coords.add(new Point3f(modelPoint [0], 0.49f, modelPoint [1]));
        pointsCount++;
      }
      it.next();
    }

    if (coords.size() > 0) {
      GeometryInfo geometryInfo = new GeometryInfo(GeometryInfo.POLYGON_ARRAY);
      geometryInfo.setCoordinates (coords.toArray(new Point3f [coords.size()]));
      int [] stripCountsArray = new int [stripCounts.size()];
      for (int i = 0; i < stripCountsArray.length; i++) {
        stripCountsArray [i] = stripCounts.get(i);
      }
      geometryInfo.setStripCounts(stripCountsArray);
      Shape3D shadow = new Shape3D(geometryInfo.getIndexedGeometryArray());
      Appearance shadowAppearance = new Appearance();
      shadowAppearance.setColoringAttributes(new ColoringAttributes(new Color3f(), ColoringAttributes.SHADE_FLAT));
      shadowAppearance.setTransparencyAttributes(new TransparencyAttributes(TransparencyAttributes.NICEST, 0.7f));
      shadow.setAppearance(shadowAppearance);   
      homeRoot.addChild(shadow);
    }
  }
}
TOP

Related Classes of com.eteks.sweethome3d.swing.HomeComponent3D

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.