/*
 * PlanComponent.java 2 juin 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.AWTKeyStroke;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.MouseInfo;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.TexturePaint;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.dnd.DragSource;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.FilteredImageSource;
import java.awt.image.MemoryImageSource;
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.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.security.AccessControlException;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.imageio.ImageIO;
import javax.media.j3d.AmbientLight;
import javax.media.j3d.Appearance;
import javax.media.j3d.Background;
import javax.media.j3d.BoundingBox;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.DirectionalLight;
import javax.media.j3d.Group;
import javax.media.j3d.ImageComponent2D;
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.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JToolTip;
import javax.swing.JViewport;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
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.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.InternationalFormatter;
import javax.swing.text.JTextComponent;
import javax.swing.text.NumberFormatter;
import javax.vecmath.Color3f;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import org.freehep.graphicsio.ImageConstants;
import org.freehep.graphicsio.svg.SVGGraphics2D;
import org.freehep.util.UserProperties;

import com.eteks.sweethome3d.j3d.Component3DManager;
import com.eteks.sweethome3d.j3d.HomePieceOfFurniture3D;
import com.eteks.sweethome3d.j3d.ModelManager;
import com.eteks.sweethome3d.j3d.TextureManager;
import com.eteks.sweethome3d.model.BackgroundImage;
import com.eteks.sweethome3d.model.Camera;
import com.eteks.sweethome3d.model.CollectionEvent;
import com.eteks.sweethome3d.model.CollectionListener;
import com.eteks.sweethome3d.model.Compass;
import com.eteks.sweethome3d.model.Content;
import com.eteks.sweethome3d.model.DimensionLine;
import com.eteks.sweethome3d.model.Elevatable;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.HomeDoorOrWindow;
import com.eteks.sweethome3d.model.HomeFurnitureGroup;
import com.eteks.sweethome3d.model.HomeLight;
import com.eteks.sweethome3d.model.HomePieceOfFurniture;
import com.eteks.sweethome3d.model.HomeTexture;
import com.eteks.sweethome3d.model.Label;
import com.eteks.sweethome3d.model.LengthUnit;
import com.eteks.sweethome3d.model.Level;
import com.eteks.sweethome3d.model.ObserverCamera;
import com.eteks.sweethome3d.model.Room;
import com.eteks.sweethome3d.model.Sash;
import com.eteks.sweethome3d.model.Selectable;
import com.eteks.sweethome3d.model.SelectionEvent;
import com.eteks.sweethome3d.model.SelectionListener;
import com.eteks.sweethome3d.model.TextStyle;
import com.eteks.sweethome3d.model.TextureImage;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.model.Wall;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.viewcontroller.PlanController;
import com.eteks.sweethome3d.viewcontroller.PlanView;
import com.eteks.sweethome3d.viewcontroller.View;
import com.sun.j3d.utils.universe.SimpleUniverse;
import com.sun.j3d.utils.universe.Viewer;
import com.sun.j3d.utils.universe.ViewingPlatform;

/**
 * A component displaying the plan of a home.
 * @author Emmanuel Puybaret
 */
public class PlanComponent extends JComponent implements PlanView, Scrollable, Printable {
  /**
   * The circumstances under which the home items displayed by this component will be painted. 
   */
  protected enum PaintMode {PAINT, PRINT, CLIPBOARD, EXPORT}
  
  private enum ActionType {DELETE_SELECTION, ESCAPE, 
      MOVE_SELECTION_LEFT, MOVE_SELECTION_UP, MOVE_SELECTION_DOWN, MOVE_SELECTION_RIGHT,
      MOVE_SELECTION_FAST_LEFT, MOVE_SELECTION_FAST_UP, MOVE_SELECTION_FAST_DOWN, MOVE_SELECTION_FAST_RIGHT,
      TOGGLE_MAGNETISM_ON, TOGGLE_MAGNETISM_OFF,
      ACTIVATE_ALIGNMENT, DEACTIVATE_ALIGNMENT,
      ACTIVATE_DUPLICATION, DEACTIVATE_DUPLICATION,       
      ACTIVATE_EDITIION, DEACTIVATE_EDITIION}
  
  private static final float    MARGIN = 40;

  private final Home            home;
  private final UserPreferences preferences;
  private float                 scale  = 0.5f;
  private boolean               selectedItemsOutlinePainted = true;
  private boolean               backgroundPainted = true;

  private PlanRulerComponent    horizontalRuler;
  private PlanRulerComponent    verticalRuler;
  
  private final Cursor          rotationCursor;
  private final Cursor          elevationCursor;
  private final Cursor          heightCursor;
  private final Cursor          powerCursor;
  private final Cursor          resizeCursor;
  private final Cursor          moveCursor;
  private final Cursor          panningCursor;
  private final Cursor          duplicationCursor;
  
  private Rectangle2D           rectangleFeedback;
  private Class<? extends Selectable> alignedObjectClass;
  private Selectable            alignedObjectFeedback;
  private Point2D               locationFeeback;
  private boolean               showPointFeedback;
  private Point2D               centerAngleFeedback;
  private Point2D               point1AngleFeedback;
  private Point2D               point2AngleFeedback;
  private List<Selectable>      draggedItemsFeedback;
  private List<DimensionLine>   dimensionLinesFeedback;
  private boolean               selectionScrollUpdated;
  private JToolTip              toolTip;
  private JWindow               toolTipWindow;
  private boolean               resizeIndicatorVisible;
  
  private Map<PlanController.EditableProperty, JFormattedTextField> toolTipEditableTextFields;
  private KeyListener                       toolTipKeyListener;
  
  private List<HomePieceOfFurniture>        sortedLevelFurniture;
  private List<Room>                        sortedLevelRooms;
  private Map<TextStyle, Font>              fonts;
  private Map<TextStyle, FontMetrics>       fontsMetrics;
  
  private Rectangle2D                       planBoundsCache;  
  private boolean                           planBoundsCacheValid = false;  
  private BufferedImage                     backgroundImageCache;
  private Map<TextureImage, BufferedImage>  patternImagesCache;
  private List<Wall>                        otherLevelWallsCache;
  private Area                              otherLevelWallAreaCache;
  private List<Room>                        otherLevelRoomsCache;
  private Area                              otherLevelRoomAreaCache;
  private Color                             wallsPatternBackgroundCache;
  private Color                             wallsPatternForegroundCache;
  private Map<Collection<Wall>, Area>       wallAreasCache;
  private Map<Content, BufferedImage>       floorTextureImagesCache;
  private Map<HomePieceOfFurniture, PieceOfFurnitureTopViewIcon> furnitureTopViewIconsCache;

  private static final Shape       POINT_INDICATOR;
  private static final GeneralPath FURNITURE_ROTATION_INDICATOR;
  private static final GeneralPath FURNITURE_RESIZE_INDICATOR;
  private static final GeneralPath FURNITURE_ELEVATION_INDICATOR;
  private static final Shape       FURNITURE_ELEVATION_POINT_INDICATOR;
  private static final GeneralPath FURNITURE_HEIGHT_INDICATOR;
  private static final Shape       FURNITURE_HEIGHT_POINT_INDICATOR;
  private static final GeneralPath LIGHT_POWER_INDICATOR;
  private static final Shape       LIGHT_POWER_POINT_INDICATOR;
  private static final GeneralPath WALL_ORIENTATION_INDICATOR;
  private static final Shape       WALL_POINT;
  private static final GeneralPath WALL_AND_LINE_RESIZE_INDICATOR;
  private static final Shape       CAMERA_YAW_ROTATION_INDICATOR;
  private static final GeneralPath CAMERA_PITCH_ROTATION_INDICATOR;
  private static final GeneralPath CAMERA_ELEVATION_INDICATOR;
  private static final Shape       CAMERA_BODY;
  private static final Shape       CAMERA_HEAD;  
  private static final GeneralPath DIMENSION_LINE_END;  
  private static final GeneralPath TEXT_LOCATION_INDICATOR;
  private static final GeneralPath TEXT_ANGLE_INDICATOR;
  private static final Shape       LABEL_CENTER_INDICATOR;
  private static final Shape       COMPASS_DISC;
  private static final GeneralPath COMPASS;
  private static final GeneralPath COMPASS_ROTATION_INDICATOR;
  private static final GeneralPath COMPASS_RESIZE_INDICATOR;
  
  private static final Stroke      INDICATOR_STROKE = new BasicStroke(1.5f);
  private static final Stroke      POINT_STROKE = new BasicStroke(2f);
  
  private static final float       WALL_STROKE_WIDTH = 1.5f;
  private static final float       BORDER_STROKE_WIDTH = 1f;
  
  private static final BufferedImage ERROR_TEXTURE_IMAGE;
  private static final BufferedImage WAIT_TEXTURE_IMAGE;

  static {
    POINT_INDICATOR = new Ellipse2D.Float(-1.5f, -1.5f, 3, 3);
    
    // Create a path that draws an round arrow used as a rotation indicator 
    // at top left point of a piece of furniture
    FURNITURE_ROTATION_INDICATOR = new GeneralPath();
    FURNITURE_ROTATION_INDICATOR.append(POINT_INDICATOR, false);
    FURNITURE_ROTATION_INDICATOR.append(new Arc2D.Float(-8, -8, 16, 16, 45, 180, Arc2D.OPEN), false);
    FURNITURE_ROTATION_INDICATOR.moveTo(2.66f, -5.66f);
    FURNITURE_ROTATION_INDICATOR.lineTo(5.66f, -5.66f);
    FURNITURE_ROTATION_INDICATOR.lineTo(4f, -8.3f);
    
    FURNITURE_ELEVATION_POINT_INDICATOR = new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f);
    
    // Create a path that draws a line with one arrow as an elevation indicator
    // at top right of a piece of furniture
    FURNITURE_ELEVATION_INDICATOR = new GeneralPath();
    FURNITURE_ELEVATION_INDICATOR.moveTo(0, -5); // Vertical line
    FURNITURE_ELEVATION_INDICATOR.lineTo(0, 5);
    FURNITURE_ELEVATION_INDICATOR.moveTo(-2.5f, 5);    // Bottom line
    FURNITURE_ELEVATION_INDICATOR.lineTo(2.5f, 5);
    FURNITURE_ELEVATION_INDICATOR.moveTo(-1.2f, 1.5f); // Bottom arrow
    FURNITURE_ELEVATION_INDICATOR.lineTo(0, 4.5f);
    FURNITURE_ELEVATION_INDICATOR.lineTo(1.2f, 1.5f);
    
    FURNITURE_HEIGHT_POINT_INDICATOR = new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f);
    
    // Create a path that draws a line with two arrows as a height indicator
    // at bottom left of a piece of furniture
    FURNITURE_HEIGHT_INDICATOR = new GeneralPath();
    FURNITURE_HEIGHT_INDICATOR.moveTo(0, -6); // Vertical line
    FURNITURE_HEIGHT_INDICATOR.lineTo(0, 6);
    FURNITURE_HEIGHT_INDICATOR.moveTo(-2.5f, -6);    // Top line
    FURNITURE_HEIGHT_INDICATOR.lineTo(2.5f, -6);
    FURNITURE_HEIGHT_INDICATOR.moveTo(-2.5f, 6);     // Bottom line
    FURNITURE_HEIGHT_INDICATOR.lineTo(2.5f, 6);
    FURNITURE_HEIGHT_INDICATOR.moveTo(-1.2f, -2.5f); // Top arrow
    FURNITURE_HEIGHT_INDICATOR.lineTo(0f, -5.5f);
    FURNITURE_HEIGHT_INDICATOR.lineTo(1.2f, -2.5f);
    FURNITURE_HEIGHT_INDICATOR.moveTo(-1.2f, 2.5f);  // Bottom arrow
    FURNITURE_HEIGHT_INDICATOR.lineTo(0f, 5.5f);
    FURNITURE_HEIGHT_INDICATOR.lineTo(1.2f, 2.5f);
    
    LIGHT_POWER_POINT_INDICATOR = new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f);
    
    // Create a path that draws a stripped triangle as a power indicator
    // at bottom left of a not deformable lights
    LIGHT_POWER_INDICATOR = new GeneralPath();
    LIGHT_POWER_INDICATOR.moveTo(-8, 0); 
    LIGHT_POWER_INDICATOR.lineTo(-6f, 0);
    LIGHT_POWER_INDICATOR.lineTo(-6f, -1);    
    LIGHT_POWER_INDICATOR.closePath();    
    LIGHT_POWER_INDICATOR.moveTo(-3, 0); 
    LIGHT_POWER_INDICATOR.lineTo(-1f, 0);
    LIGHT_POWER_INDICATOR.lineTo(-1f, -2.5f);    
    LIGHT_POWER_INDICATOR.lineTo(-3f, -1.8f);    
    LIGHT_POWER_INDICATOR.closePath();    
    LIGHT_POWER_INDICATOR.moveTo(2, 0); 
    LIGHT_POWER_INDICATOR.lineTo(4, 0);
    LIGHT_POWER_INDICATOR.lineTo(4f, -3.5f);    
    LIGHT_POWER_INDICATOR.lineTo(2f, -2.8f);    
    LIGHT_POWER_INDICATOR.closePath();    

    // Create a path used as a resize indicator 
    // at bottom right point of a piece of furniture
    FURNITURE_RESIZE_INDICATOR = new GeneralPath();
    FURNITURE_RESIZE_INDICATOR.append(new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f), false);
    FURNITURE_RESIZE_INDICATOR.moveTo(5, -4);
    FURNITURE_RESIZE_INDICATOR.lineTo(7, -4);
    FURNITURE_RESIZE_INDICATOR.lineTo(7, 7);
    FURNITURE_RESIZE_INDICATOR.lineTo(-4, 7);
    FURNITURE_RESIZE_INDICATOR.lineTo(-4, 5);
    FURNITURE_RESIZE_INDICATOR.moveTo(3.5f, 3.5f);
    FURNITURE_RESIZE_INDICATOR.lineTo(9, 9);
    FURNITURE_RESIZE_INDICATOR.moveTo(7, 9.5f);
    FURNITURE_RESIZE_INDICATOR.lineTo(10, 10);
    FURNITURE_RESIZE_INDICATOR.lineTo(9.5f, 7);
    
    // Create a path used an orientation indicator
    // at start and end points of a selected wall
    WALL_ORIENTATION_INDICATOR = new GeneralPath();
    WALL_ORIENTATION_INDICATOR.moveTo(-4, -4);
    WALL_ORIENTATION_INDICATOR.lineTo(4, 0);
    WALL_ORIENTATION_INDICATOR.lineTo(-4, 4);

    WALL_POINT = new Ellipse2D.Float(-3, -3, 6, 6);

    // Create a path used as a size indicator 
    // at start and end points of a selected wall
    WALL_AND_LINE_RESIZE_INDICATOR = new GeneralPath();
    WALL_AND_LINE_RESIZE_INDICATOR.moveTo(5, -2);
    WALL_AND_LINE_RESIZE_INDICATOR.lineTo(5, 2);
    WALL_AND_LINE_RESIZE_INDICATOR.moveTo(6, 0);
    WALL_AND_LINE_RESIZE_INDICATOR.lineTo(11, 0);
    WALL_AND_LINE_RESIZE_INDICATOR.moveTo(8.7f, -1.8f);
    WALL_AND_LINE_RESIZE_INDICATOR.lineTo(12, 0);
    WALL_AND_LINE_RESIZE_INDICATOR.lineTo(8.7f, 1.8f);
    
    // Create a path used as yaw rotation indicator for the camera
    AffineTransform transform = AffineTransform.getRotateInstance(-Math.PI / 4);
    CAMERA_YAW_ROTATION_INDICATOR = FURNITURE_ROTATION_INDICATOR.createTransformedShape(transform);
    
    // Create a path used as pitch rotation indicator for the camera
    CAMERA_PITCH_ROTATION_INDICATOR = new GeneralPath();
    CAMERA_PITCH_ROTATION_INDICATOR.append(POINT_INDICATOR, false);
    CAMERA_PITCH_ROTATION_INDICATOR.moveTo(4.5f, 0);
    CAMERA_PITCH_ROTATION_INDICATOR.lineTo(5.2f, 0);
    CAMERA_PITCH_ROTATION_INDICATOR.moveTo(9f, 0);
    CAMERA_PITCH_ROTATION_INDICATOR.lineTo(10, 0);
    CAMERA_PITCH_ROTATION_INDICATOR.append(new Arc2D.Float(7, -8, 5, 16, 20, 320, Arc2D.OPEN), false);
    CAMERA_PITCH_ROTATION_INDICATOR.moveTo(10f, 4.5f);
    CAMERA_PITCH_ROTATION_INDICATOR.lineTo(12.3f, 2f);
    CAMERA_PITCH_ROTATION_INDICATOR.lineTo(12.8f, 5.8f);
    
    // Create a path that draws a line with one arrow as an elevation indicator
    // at the back of the camera
    CAMERA_ELEVATION_INDICATOR = new GeneralPath();
    CAMERA_ELEVATION_INDICATOR.moveTo(0, -4); // Vertical line
    CAMERA_ELEVATION_INDICATOR.lineTo(0, 4);
    CAMERA_ELEVATION_INDICATOR.moveTo(-2.5f, 4);    // Bottom line
    CAMERA_ELEVATION_INDICATOR.lineTo(2.5f, 4);
    CAMERA_ELEVATION_INDICATOR.moveTo(-1.2f, 0.5f); // Bottom arrow
    CAMERA_ELEVATION_INDICATOR.lineTo(0, 3.5f);
    CAMERA_ELEVATION_INDICATOR.lineTo(1.2f, 0.5f);

    // Create a path used to draw the camera 
    // This path looks like a human being seen from top that fits in one cm wide square 
    GeneralPath cameraBodyAreaPath = new GeneralPath();
    cameraBodyAreaPath.append(new Ellipse2D.Float(-0.5f, -0.425f, 1f, 0.85f), false); // Body
    cameraBodyAreaPath.append(new Ellipse2D.Float(-0.5f, -0.3f, 0.24f, 0.6f), false); // Shoulder
    cameraBodyAreaPath.append(new Ellipse2D.Float(0.26f, -0.3f, 0.24f, 0.6f), false); // Shoulder
    CAMERA_BODY = new Area(cameraBodyAreaPath);
    
    GeneralPath cameraHeadAreaPath = new GeneralPath();
    cameraHeadAreaPath.append(new Ellipse2D.Float(-0.18f, -0.45f, 0.36f, 1f), false); // Head
    cameraHeadAreaPath.moveTo(-0.04f, 0.55f); // Noise
    cameraHeadAreaPath.lineTo(0, 0.65f);
    cameraHeadAreaPath.lineTo(0.04f, 0.55f);
    cameraHeadAreaPath.closePath();
    CAMERA_HEAD = new Area(cameraHeadAreaPath);
    
    DIMENSION_LINE_END = new GeneralPath();
    DIMENSION_LINE_END.moveTo(-5, 5);
    DIMENSION_LINE_END.lineTo(5, -5);
    DIMENSION_LINE_END.moveTo(0, 5);
    DIMENSION_LINE_END.lineTo(0, -5);
    
    // Create a path that draws three arrows going left, right and down
    TEXT_LOCATION_INDICATOR = new GeneralPath();
    TEXT_LOCATION_INDICATOR.append(new Arc2D.Float(-2, 0, 4, 4, 190, 160, Arc2D.CHORD), false);
    TEXT_LOCATION_INDICATOR.moveTo(0, 4);        // Down line
    TEXT_LOCATION_INDICATOR.lineTo(0, 12);
    TEXT_LOCATION_INDICATOR.moveTo(-1.2f, 8.5f); // Down arrow
    TEXT_LOCATION_INDICATOR.lineTo(0f, 11.5f);
    TEXT_LOCATION_INDICATOR.lineTo(1.2f, 8.5f);
    TEXT_LOCATION_INDICATOR.moveTo(2f, 3f);      // Right line
    TEXT_LOCATION_INDICATOR.lineTo(9, 6);
    TEXT_LOCATION_INDICATOR.moveTo(6, 6.5f);     // Right arrow
    TEXT_LOCATION_INDICATOR.lineTo(10, 7);
    TEXT_LOCATION_INDICATOR.lineTo(7.5f, 3.5f);
    TEXT_LOCATION_INDICATOR.moveTo(-2f, 3f);     // Left line
    TEXT_LOCATION_INDICATOR.lineTo(-9, 6);
    TEXT_LOCATION_INDICATOR.moveTo(-6, 6.5f);    // Left arrow
    TEXT_LOCATION_INDICATOR.lineTo(-10, 7);
    TEXT_LOCATION_INDICATOR.lineTo(-7.5f, 3.5f);
    
    // Create a path used as angle indicator for texts
    TEXT_ANGLE_INDICATOR = new GeneralPath(); 
    TEXT_ANGLE_INDICATOR.append(new Arc2D.Float(-1.25f, -1.25f, 2.5f, 2.5f, 10, 160, Arc2D.CHORD), false);    
    TEXT_ANGLE_INDICATOR.append(new Arc2D.Float(-8, -8, 16, 16, 30, 120, Arc2D.OPEN), false);
    TEXT_ANGLE_INDICATOR.moveTo(4f, -5.2f);
    TEXT_ANGLE_INDICATOR.lineTo(6.9f, -4f);
    TEXT_ANGLE_INDICATOR.lineTo(5.8f, -7f);
    
    LABEL_CENTER_INDICATOR = new Ellipse2D.Float(-1f, -1f, 2, 2);
    
    // Create the path used to draw the compass
    COMPASS_DISC = new Ellipse2D.Float(-0.5f, -0.5f, 1, 1);
    BasicStroke stroke = new BasicStroke(0.01f);
    COMPASS = new GeneralPath(stroke.createStrokedShape(COMPASS_DISC));
    COMPASS.append(stroke.createStrokedShape(new Line2D.Float(-0.6f, 0, -0.5f, 0)), false);
    COMPASS.append(stroke.createStrokedShape(new Line2D.Float(0.6f, 0, 0.5f, 0)), false);
    COMPASS.append(stroke.createStrokedShape(new Line2D.Float(0, 0.6f, 0, 0.5f)), false);
    stroke = new BasicStroke(0.04f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
    COMPASS.append(stroke.createStrokedShape(new Line2D.Float(0, 0, 0, 0)), false);
    GeneralPath compassNeedle = new GeneralPath();
    compassNeedle.moveTo(0, -0.47f);
    compassNeedle.lineTo(0.15f, 0.46f);
    compassNeedle.lineTo(0, 0.32f);
    compassNeedle.lineTo(-0.15f, 0.46f);
    compassNeedle.closePath();
    stroke = new BasicStroke(0.03f);
    COMPASS.append(stroke.createStrokedShape(compassNeedle), false);
    GeneralPath compassNorthDirection = new GeneralPath();
    compassNorthDirection.moveTo(-0.07f, -0.55f); // Draws the N letter
    compassNorthDirection.lineTo(-0.07f, -0.69f);
    compassNorthDirection.lineTo(0.07f, -0.56f);
    compassNorthDirection.lineTo(0.07f, -0.7f);
    COMPASS.append(stroke.createStrokedShape(compassNorthDirection), false);

    // Create a path used as rotation indicator for the compass
    COMPASS_ROTATION_INDICATOR = new GeneralPath();
    COMPASS_ROTATION_INDICATOR.append(POINT_INDICATOR, false);
    COMPASS_ROTATION_INDICATOR.append(new Arc2D.Float(-8, -7, 16, 16, 210, 120, Arc2D.OPEN), false);
    COMPASS_ROTATION_INDICATOR.moveTo(4f, 5.66f);
    COMPASS_ROTATION_INDICATOR.lineTo(7f, 5.66f);
    COMPASS_ROTATION_INDICATOR.lineTo(5.6f, 8.3f);
    
    // Create a path used as a resize indicator for the compass 
    COMPASS_RESIZE_INDICATOR = new GeneralPath();
    COMPASS_RESIZE_INDICATOR.append(new Rectangle2D.Float(-1.5f, -1.5f, 3f, 3f), false);
    COMPASS_RESIZE_INDICATOR.moveTo(4, -6);
    COMPASS_RESIZE_INDICATOR.lineTo(6, -6);
    COMPASS_RESIZE_INDICATOR.lineTo(6, 6);
    COMPASS_RESIZE_INDICATOR.lineTo(4, 6);
    COMPASS_RESIZE_INDICATOR.moveTo(5, 0);
    COMPASS_RESIZE_INDICATOR.lineTo(9, 0);
    COMPASS_RESIZE_INDICATOR.moveTo(9, -1.5f);
    COMPASS_RESIZE_INDICATOR.lineTo(12, 0);
    COMPASS_RESIZE_INDICATOR.lineTo(9, 1.5f);
    
    ERROR_TEXTURE_IMAGE = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
    Graphics g = ERROR_TEXTURE_IMAGE.getGraphics();
    g.setColor(Color.RED);
    g.drawLine(0, 0, 0, 0);
    g.dispose();

    WAIT_TEXTURE_IMAGE = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
    g = WAIT_TEXTURE_IMAGE.getGraphics();
    g.setColor(Color.WHITE);
    g.drawLine(0, 0, 0, 0);
    g.dispose();
  }

  /**
   * Creates a new plan that displays <code>home</code>.
   */
  public PlanComponent(Home home, 
                       UserPreferences preferences,
                       PlanController controller) {
    this.home = home;
    this.preferences = preferences;
    // Set JComponent default properties
    setOpaque(true);
    // Add listeners
    addModelListeners(home, preferences, controller);
    createToolTipTextFields(preferences, controller);
    if (controller != null) {
      addMouseListeners(controller);
      addFocusListener(controller);
      createActions(controller);      
      installDefaultKeyboardActions();
      setFocusable(true);
      setAutoscrolls(true);
    }
    this.rotationCursor = createCustomCursor("resources/cursors/rotation16x16.png",
        "resources/cursors/rotation32x32.png", "Rotation cursor", Cursor.MOVE_CURSOR);
    this.elevationCursor = createCustomCursor("resources/cursors/elevation16x16.png",
        "resources/cursors/elevation32x32.png", "Elevation cursor", Cursor.MOVE_CURSOR);
    this.heightCursor = createCustomCursor("resources/cursors/height16x16.png",
        "resources/cursors/height32x32.png", "Height cursor", Cursor.MOVE_CURSOR);
    this.powerCursor = createCustomCursor("resources/cursors/power16x16.png",
        "resources/cursors/power32x32.png", "Power cursor", Cursor.MOVE_CURSOR);
    this.resizeCursor = createCustomCursor("resources/cursors/resize16x16.png",
        "resources/cursors/resize32x32.png", "Resize cursor", Cursor.MOVE_CURSOR);
    this.moveCursor = createCustomCursor("resources/cursors/move16x16.png",
        "resources/cursors/move32x32.png", "Move cursor", Cursor.MOVE_CURSOR);
    this.panningCursor = createCustomCursor("resources/cursors/panning16x16.png",
        "resources/cursors/panning32x32.png", "Panning cursor", Cursor.HAND_CURSOR);
    this.duplicationCursor = DragSource.DefaultCopyDrop;
    this.patternImagesCache = new HashMap<TextureImage, BufferedImage>();
    // Install default colors using same colors as a text field
    super.setForeground(UIManager.getColor("TextField.foreground"));
    super.setBackground(UIManager.getColor("TextField.background"));
  }

  /**
   * Adds home items and selection listeners on this component to receive  
   * changes notifications from home. 
   */
  private void addModelListeners(Home home, final UserPreferences preferences, 
                                 final PlanController controller) {
    // Add listener to update plan when furniture changes
    final PropertyChangeListener furnitureChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          if (furnitureTopViewIconsCache != null
              && (HomePieceOfFurniture.Property.COLOR.name().equals(ev.getPropertyName())
                  || HomePieceOfFurniture.Property.TEXTURE.name().equals(ev.getPropertyName())
                  || HomePieceOfFurniture.Property.MODEL_MATERIALS.name().equals(ev.getPropertyName())
                  || HomePieceOfFurniture.Property.SHININESS.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)ev.getSource()).getTexture() != null)) {
            for (HomePieceOfFurniture piece : getFurnitureWithoutGroups((HomePieceOfFurniture)ev.getSource())) {
              furnitureTopViewIconsCache.remove(piece);
            }
            repaint();
          } else if (HomePieceOfFurniture.Property.ELEVATION.name().equals(ev.getPropertyName())
                     || HomePieceOfFurniture.Property.LEVEL.name().equals(ev.getPropertyName())
                     || HomePieceOfFurniture.Property.HEIGHT.name().equals(ev.getPropertyName())) {
            sortedLevelFurniture = null;
            repaint();
          } else {
            revalidate();
          }
        }
      };
    for (HomePieceOfFurniture piece : home.getFurniture()) {
      piece.addPropertyChangeListener(furnitureChangeListener);
    }
    home.addFurnitureListener(new CollectionListener<HomePieceOfFurniture>() {
        public void collectionChanged(CollectionEvent<HomePieceOfFurniture> ev) {
          if (ev.getType() == CollectionEvent.Type.ADD) {
            ev.getItem().addPropertyChangeListener(furnitureChangeListener);
          } else if (ev.getType() == CollectionEvent.Type.DELETE) {
            ev.getItem().removePropertyChangeListener(furnitureChangeListener);
          }
          sortedLevelFurniture = null;
          revalidate();
        }
      });
    
    // Add listener to update plan when walls change
    final PropertyChangeListener wallChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          String propertyName = ev.getPropertyName();
          if (Wall.Property.X_START.name().equals(propertyName)
              || Wall.Property.X_END.name().equals(propertyName) 
              || Wall.Property.Y_START.name().equals(propertyName) 
              || Wall.Property.Y_END.name().equals(propertyName)
              || Wall.Property.WALL_AT_START.name().equals(propertyName)
              || Wall.Property.WALL_AT_END.name().equals(propertyName)
              || Wall.Property.THICKNESS.name().equals(propertyName)
              || Wall.Property.ARC_EXTENT.name().equals(propertyName)
              || Wall.Property.PATTERN.name().equals(propertyName)) {
            wallAreasCache = null;
            revalidate();
          } else if (Wall.Property.LEVEL.name().equals(propertyName)
              || Wall.Property.HEIGHT.name().equals(propertyName)
              || Wall.Property.HEIGHT_AT_END.name().equals(propertyName)) {
            otherLevelWallAreaCache = null;
            otherLevelWallsCache = null;
            wallAreasCache = null;
            repaint();
          }
        }
      };
    for (Wall wall : home.getWalls()) {
      wall.addPropertyChangeListener(wallChangeListener);
    }
    home.addWallsListener(new CollectionListener<Wall> () {
        public void collectionChanged(CollectionEvent<Wall> ev) {
          if (ev.getType() == CollectionEvent.Type.ADD) {
            ev.getItem().addPropertyChangeListener(wallChangeListener);
          } else if (ev.getType() == CollectionEvent.Type.DELETE) {
            ev.getItem().removePropertyChangeListener(wallChangeListener);
          }
          otherLevelWallAreaCache = null;
          otherLevelWallsCache = null;
          wallAreasCache = null;
          revalidate();
        }
      });
    
    // Add listener to update plan when rooms change
    final PropertyChangeListener roomChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          String propertyName = ev.getPropertyName();
          if (Room.Property.POINTS.name().equals(propertyName)
              || Room.Property.NAME.name().equals(propertyName)
              || Room.Property.NAME_X_OFFSET.name().equals(propertyName)
              || Room.Property.NAME_Y_OFFSET.name().equals(propertyName)
              || Room.Property.NAME_STYLE.name().equals(propertyName)
              || Room.Property.NAME_ANGLE.name().equals(propertyName)
              || Room.Property.AREA_VISIBLE.name().equals(propertyName)
              || Room.Property.AREA_X_OFFSET.name().equals(propertyName)
              || Room.Property.AREA_Y_OFFSET.name().equals(propertyName)
              || Room.Property.AREA_STYLE.name().equals(propertyName)
              || Room.Property.AREA_ANGLE.name().equals(propertyName)) {
            sortedLevelRooms = null;
            otherLevelRoomsCache = null;
            otherLevelRoomAreaCache = null;
            revalidate();
          } else if (preferences.isRoomFloorColoredOrTextured()
                     && (Room.Property.FLOOR_COLOR.name().equals(propertyName)
                         || Room.Property.FLOOR_TEXTURE.name().equals(propertyName)
                         || Room.Property.FLOOR_VISIBLE.name().equals(propertyName))) {
            repaint(); 
          }
        }
      };
    for (Room room : home.getRooms()) {
      room.addPropertyChangeListener(roomChangeListener);
    }
    home.addRoomsListener(new CollectionListener<Room> () {
        public void collectionChanged(CollectionEvent<Room> ev) {
          if (ev.getType() == CollectionEvent.Type.ADD) {
            ev.getItem().addPropertyChangeListener(roomChangeListener);
          } else if (ev.getType() == CollectionEvent.Type.DELETE) {
            ev.getItem().removePropertyChangeListener(roomChangeListener);
          }
          sortedLevelRooms = null;
          otherLevelRoomsCache = null;
          otherLevelRoomAreaCache = null;
          revalidate();
        }
      });

    // Add listener to update plan when dimension lines change
    final PropertyChangeListener dimensionLineChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          revalidate();
        }
      };
    for (DimensionLine dimensionLine : home.getDimensionLines()) {
      dimensionLine.addPropertyChangeListener(dimensionLineChangeListener);
    }
    home.addDimensionLinesListener(new CollectionListener<DimensionLine> () {
        public void collectionChanged(CollectionEvent<DimensionLine> ev) {
          if (ev.getType() == CollectionEvent.Type.ADD) {
            ev.getItem().addPropertyChangeListener(dimensionLineChangeListener);
          } else if (ev.getType() == CollectionEvent.Type.DELETE) {
            ev.getItem().removePropertyChangeListener(dimensionLineChangeListener);
          }
          revalidate();
        }
      });

    // Add listener to update plan when labels change
    final PropertyChangeListener labelChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          revalidate();
        }
      };
    for (Label label : home.getLabels()) {
      label.addPropertyChangeListener(labelChangeListener);
    }
    home.addLabelsListener(new CollectionListener<Label> () {
        public void collectionChanged(CollectionEvent<Label> ev) {
          if (ev.getType() == CollectionEvent.Type.ADD) {
            ev.getItem().addPropertyChangeListener(labelChangeListener);
          } else if (ev.getType() == CollectionEvent.Type.DELETE) {
            ev.getItem().removePropertyChangeListener(labelChangeListener);
          }
          revalidate();
        }
      });

    // Add listener to update plan when levels change
    final PropertyChangeListener levelChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          String propertyName = ev.getPropertyName();
          if (Level.Property.BACKGROUND_IMAGE.name().equals(propertyName)) {
            backgroundImageCache = null;
            revalidate();
          } else if (Level.Property.ELEVATION.name().equals(propertyName)) {
            otherLevelWallAreaCache = null;
            otherLevelWallsCache = null;
            otherLevelRoomsCache = null;
            otherLevelRoomAreaCache = null;
            wallAreasCache = null;
            sortedLevelFurniture = null;
            repaint();
          }
        }
      };
    for (Level level : home.getLevels()) {
      level.addPropertyChangeListener(levelChangeListener);
    }
    home.addLevelsListener(new CollectionListener<Level> () {
        public void collectionChanged(CollectionEvent<Level> ev) {
          Level level = ev.getItem();
          if (ev.getType() == CollectionEvent.Type.ADD) {
            level.addPropertyChangeListener(levelChangeListener);
          } else if (ev.getType() == CollectionEvent.Type.DELETE) {
            level.removePropertyChangeListener(levelChangeListener);
          }
          revalidate();
        }
      });

    home.addPropertyChangeListener(Home.Property.CAMERA, new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          revalidate();
        }
      });
    home.getObserverCamera().addPropertyChangeListener(new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          String propertyName = ev.getPropertyName();
          if (Camera.Property.X.name().equals(propertyName)
              || Camera.Property.Y.name().equals(propertyName) 
              || Camera.Property.FIELD_OF_VIEW.name().equals(propertyName)
              || Camera.Property.YAW.name().equals(propertyName)
              || ObserverCamera.Property.WIDTH.name().equals(propertyName)
              || ObserverCamera.Property.DEPTH.name().equals(propertyName) 
              || ObserverCamera.Property.HEIGHT.name().equals(propertyName)) {
            revalidate();
          }
        }
      });
    home.getCompass().addPropertyChangeListener(new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          String propertyName = ev.getPropertyName();
          if (Compass.Property.X.name().equals(propertyName)
              || Compass.Property.Y.name().equals(propertyName) 
              || Compass.Property.NORTH_DIRECTION.name().equals(propertyName)
              || Compass.Property.DIAMETER.name().equals(propertyName)
              || Compass.Property.VISIBLE.name().equals(propertyName)) {
            revalidate();
          }
        }
      });
    home.addSelectionListener(new SelectionListener () {
        public void selectionChanged(SelectionEvent ev) {
          repaint();
        }
      });
    home.addPropertyChangeListener(Home.Property.BACKGROUND_IMAGE, 
      new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          backgroundImageCache = null;
          repaint();
        }
      });
    home.addPropertyChangeListener(Home.Property.SELECTED_LEVEL, new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent ev) {
          backgroundImageCache = null;
          otherLevelWallAreaCache = null;
          otherLevelWallsCache = null;
          otherLevelRoomsCache = null;
          otherLevelRoomAreaCache = null;
          wallAreasCache = null;
          sortedLevelRooms = null;
          sortedLevelFurniture = null;
          repaint();
        }
      });
    preferences.addPropertyChangeListener(UserPreferences.Property.UNIT, 
        new UserPreferencesChangeListener(this));
    preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE, 
        new UserPreferencesChangeListener(this));
    preferences.addPropertyChangeListener(UserPreferences.Property.GRID_VISIBLE, 
        new UserPreferencesChangeListener(this));
    preferences.addPropertyChangeListener(UserPreferences.Property.FURNITURE_VIEWED_FROM_TOP, 
        new UserPreferencesChangeListener(this));
    preferences.addPropertyChangeListener(UserPreferences.Property.ROOM_FLOOR_COLORED_OR_TEXTURED, 
        new UserPreferencesChangeListener(this));
    preferences.addPropertyChangeListener(UserPreferences.Property.WALL_PATTERN, 
        new UserPreferencesChangeListener(this));
  }

  /**
   * Returns all the pieces depending on the given <code>piece</code> that are not groups.  
   */
  private List<HomePieceOfFurniture> getFurnitureWithoutGroups(HomePieceOfFurniture piece) {
    if (piece instanceof HomeFurnitureGroup) {
      List<HomePieceOfFurniture> pieces = new ArrayList<HomePieceOfFurniture>();
      for (HomePieceOfFurniture groupPiece : ((HomeFurnitureGroup)piece).getFurniture()) {
        pieces.addAll(getFurnitureWithoutGroups(groupPiece));
      }
      return pieces;
    } else {
      return Arrays.asList(new HomePieceOfFurniture [] {piece});
    }
  }

  /**
   * Preferences property listener bound to this component with a weak reference to avoid
   * strong link between preferences and this component.  
   */
  private static class UserPreferencesChangeListener implements PropertyChangeListener {
    private WeakReference<PlanComponent>  planComponent;

    public UserPreferencesChangeListener(PlanComponent planComponent) {
      this.planComponent = new WeakReference<PlanComponent>(planComponent);
    }
    
    public void propertyChange(PropertyChangeEvent ev) {
      // If plan component was garbage collected, remove this listener from preferences
      PlanComponent planComponent = this.planComponent.get();
      UserPreferences preferences = (UserPreferences)ev.getSource();
      UserPreferences.Property property = UserPreferences.Property.valueOf(ev.getPropertyName());
      if (planComponent == null) {
        preferences.removePropertyChangeListener(property, this);
      } else {
        switch (property) {
          case LANGUAGE :
          case UNIT :
            // Update format of tool tip text fields
            for (Map.Entry<PlanController.EditableProperty, JFormattedTextField> toolTipTextFieldEntry : 
              planComponent.toolTipEditableTextFields.entrySet()) {
              updateToolTipTextFieldFormatterFactory(toolTipTextFieldEntry.getValue(), 
                  toolTipTextFieldEntry.getKey(), preferences);
            }
            if (planComponent.horizontalRuler != null) {
              planComponent.horizontalRuler.repaint();
            }
            if (planComponent.verticalRuler != null) {
              planComponent.verticalRuler.repaint();
            }
            break;
          case WALL_PATTERN :
            planComponent.wallAreasCache = null;
            break;
          case FURNITURE_VIEWED_FROM_TOP :
            if (planComponent.furnitureTopViewIconsCache != null
                && !preferences.isFurnitureViewedFromTop()) {
              planComponent.furnitureTopViewIconsCache = null;
            }
            break;
        }
        planComponent.repaint();
      }
    }
  }

  /**
   * Revalidates and repaints this component and its rulers.
   */
  @Override
  public void revalidate() {
    revalidate(true);
  }
  
  /**
   * Revalidates this component after invalidating plan bounds cache if <code>invalidatePlanBoundsCache</code> is <code>true</code>
   * and updates viewport position if this component is displayed in a scrolled pane.
   */
  private void revalidate(boolean invalidatePlanBoundsCache) {
    boolean planBoundsCacheWereValid = this.planBoundsCacheValid;
    final float planBoundsMinX = (float)getPlanBounds().getMinX();
    final float planBoundsMinY = (float)getPlanBounds().getMinY();
    if (invalidatePlanBoundsCache
        && planBoundsCacheWereValid) {      
      this.planBoundsCacheValid = false;
    }
    
    // Revalidate and repaint
    super.revalidate();
    repaint();

    if (invalidatePlanBoundsCache
        && getParent() instanceof JViewport) {
      float planBoundsNewMinX = (float)getPlanBounds().getMinX();
      float planBoundsNewMinY = (float)getPlanBounds().getMinY();
      // If plan bounds upper left corner diminished
      if (planBoundsNewMinX < planBoundsMinX
          || planBoundsNewMinY < planBoundsMinY) {
        JViewport parent = (JViewport)getParent();
        final Point viewPosition = parent.getViewPosition();
        Dimension extentSize = parent.getExtentSize();
        Dimension viewSize = parent.getViewSize();
        // Update view position when scroll bars are visible
        if (extentSize.width < viewSize.width
            || extentSize.height < viewSize.height) {
          int deltaX = Math.round((planBoundsMinX - planBoundsNewMinX) * getScale());
          int deltaY = Math.round((planBoundsMinY - planBoundsNewMinY) * getScale());
          parent.setViewPosition(new Point(viewPosition.x + deltaX, viewPosition.y + deltaY));
        }
      }
    }

    if (this.horizontalRuler != null) {
      this.horizontalRuler.revalidate();
      this.horizontalRuler.repaint();
    }
    if (this.verticalRuler != null) {
      this.verticalRuler.revalidate();
      this.verticalRuler.repaint();
    }
  }
  
  /**
   * Adds AWT mouse listeners to this component that calls back <code>controller</code> methods.  
   */
  private void addMouseListeners(final PlanController controller) {
    MouseInputAdapter mouseListener = new MouseInputAdapter() {
      private Point lastMousePressedLocation;
      
      @Override
      public void mousePressed(MouseEvent ev) {
        this.lastMousePressedLocation = ev.getPoint();
        if (isEnabled() && !ev.isPopupTrigger()) {
          requestFocusInWindow();          
          if (ev.getButton() == MouseEvent.BUTTON1) {
            boolean alignmentActivated = OperatingSystem.isWindows() || OperatingSystem.isMacOSX() 
                ? ev.isShiftDown()
                : ev.isShiftDown() && !ev.isAltDown();
            boolean duplicationActivated = OperatingSystem.isMacOSX() 
                ? ev.isAltDown() 
                : ev.isControlDown();
            boolean magnetismToggled = OperatingSystem.isWindows() 
                ? ev.isAltDown() 
                : (OperatingSystem.isMacOSX() 
                       ? ev.isMetaDown()
                       : ev.isShiftDown() && ev.isAltDown());
            controller.pressMouse(convertXPixelToModel(ev.getX()), convertYPixelToModel(ev.getY()), 
                ev.getClickCount(), ev.isShiftDown() && !ev.isControlDown() && !ev.isAltDown() && !ev.isMetaDown(), 
                alignmentActivated, duplicationActivated, magnetismToggled);
          }
        }
      }

      @Override
      public void mouseReleased(MouseEvent ev) {
        if (isEnabled() && !ev.isPopupTrigger() && ev.getButton() == MouseEvent.BUTTON1) {
          controller.releaseMouse(convertXPixelToModel(ev.getX()), convertYPixelToModel(ev.getY()));
        }
      }

      @Override
      public void mouseMoved(MouseEvent ev) {
        // Ignore mouseMoved events that follows a mousePressed at the same location (Linux notifies this kind of events)
        if (this.lastMousePressedLocation != null
            && !this.lastMousePressedLocation.equals(ev.getPoint())) {
          this.lastMousePressedLocation = null;
        }
        if (this.lastMousePressedLocation == null) {
          if (isEnabled()) {
            controller.moveMouse(convertXPixelToModel(ev.getX()), convertYPixelToModel(ev.getY()));
          }
        }
      }

      @Override
      public void mouseDragged(MouseEvent ev) {
        if (isEnabled()) {
          mouseMoved(ev);
        }
      }
    };
    addMouseListener(mouseListener);
    addMouseMotionListener(mouseListener);
    addMouseWheelListener(new MouseWheelListener() {        
        public void mouseWheelMoved(MouseWheelEvent ev) {
          if (ev.getModifiers() == getToolkit().getMenuShortcutKeyMask()) {
            float mouseX = 0;
            float mouseY = 0;
            int deltaX = 0;
            int deltaY = 0;
            if (getParent() instanceof JViewport) {
              mouseX = convertXPixelToModel(ev.getX());
              mouseY = convertYPixelToModel(ev.getY());
              Rectangle viewRectangle = ((JViewport)getParent()).getViewRect();
              deltaX = ev.getX() - viewRectangle.x;
              deltaY = ev.getY() - viewRectangle.y;
            }

            float oldScale = getScale();
            controller.zoom((float)(ev.getWheelRotation() < 0 
                ? Math.pow(1.05, -ev.getWheelRotation()) 
                : Math.pow(0.95, ev.getWheelRotation())));
            
            if (getScale() != oldScale && getParent() instanceof JViewport) {
              // If scale changed, update viewport position to keep the same coordinates under mouse cursor
              ((JViewport)getParent()).setViewPosition(new Point());
              moveView(mouseX - convertXPixelToModel(deltaX), mouseY - convertYPixelToModel(deltaY));
            }
          } else if (getMouseWheelListeners().length == 1) {
            // If this listener is the only one registered on this component
            // redispatch event to its parent (for default scroll bar management)
            getParent().dispatchEvent(
              new MouseWheelEvent(getParent(), ev.getID(), ev.getWhen(), 
                  ev.getModifiersEx() | ev.getModifiers(), 
                  ev.getX() - getX(), ev.getY() - getY(), 
                  ev.getClickCount(), ev.isPopupTrigger(), ev.getScrollType(), 
                  ev.getScrollAmount(), ev.getWheelRotation()));
          }
        }
      });
  }

  /**
   * Adds AWT focus listener to this component that calls back <code>controller</code> 
   * escape method on focus lost event.  
   */
  private void addFocusListener(final PlanController controller) {
    addFocusListener(new FocusAdapter() {
        @Override
        public void focusLost(FocusEvent ev) {
          controller.escape();
        }
      });
    
    if (OperatingSystem.isMacOSXLeopardOrSuperior()) {
      addPropertyChangeListener("Frame.active", new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent ev) {
            if (!home.getSelectedItems().isEmpty()) {
              // Repaint to update selection color
              repaint();
            }
          }
        });
    }
  }
  
  /**
   * Installs default keys bound to actions. 
   */
  private void installDefaultKeyboardActions() {
    InputMap inputMap = getInputMap(WHEN_FOCUSED);
    inputMap.clear();
    inputMap.put(KeyStroke.getKeyStroke("DELETE"), ActionType.DELETE_SELECTION);
    inputMap.put(KeyStroke.getKeyStroke("BACK_SPACE"), ActionType.DELETE_SELECTION);
    inputMap.put(KeyStroke.getKeyStroke("ESCAPE"), ActionType.ESCAPE);
    inputMap.put(KeyStroke.getKeyStroke("shift ESCAPE"), ActionType.ESCAPE);
    inputMap.put(KeyStroke.getKeyStroke("LEFT"), ActionType.MOVE_SELECTION_LEFT);
    inputMap.put(KeyStroke.getKeyStroke("shift LEFT"), ActionType.MOVE_SELECTION_FAST_LEFT);
    inputMap.put(KeyStroke.getKeyStroke("UP"), ActionType.MOVE_SELECTION_UP);
    inputMap.put(KeyStroke.getKeyStroke("shift UP"), ActionType.MOVE_SELECTION_FAST_UP);
    inputMap.put(KeyStroke.getKeyStroke("DOWN"), ActionType.MOVE_SELECTION_DOWN);
    inputMap.put(KeyStroke.getKeyStroke("shift DOWN"), ActionType.MOVE_SELECTION_FAST_DOWN);
    inputMap.put(KeyStroke.getKeyStroke("RIGHT"), ActionType.MOVE_SELECTION_RIGHT);
    inputMap.put(KeyStroke.getKeyStroke("shift RIGHT"), ActionType.MOVE_SELECTION_FAST_RIGHT);
    inputMap.put(KeyStroke.getKeyStroke("ENTER"), ActionType.ACTIVATE_EDITIION);
    inputMap.put(KeyStroke.getKeyStroke("shift ENTER"), ActionType.ACTIVATE_EDITIION);

    if (OperatingSystem.isMacOSX()) {
      // Under Mac OS X, duplication with Alt key 
      inputMap.put(KeyStroke.getKeyStroke("alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("released ALT"), ActionType.DEACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift released ALT"), ActionType.DEACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("meta alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("meta released ALT"), ActionType.DEACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift meta alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift meta released ALT"), ActionType.DEACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("alt ESCAPE"), ActionType.ESCAPE);
      inputMap.put(KeyStroke.getKeyStroke("alt ENTER"), ActionType.ACTIVATE_EDITIION);
    } else {
      // Under other systems, duplication with Ctrl key 
      inputMap.put(KeyStroke.getKeyStroke("control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("meta control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("meta released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift meta control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift meta released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("control ESCAPE"), ActionType.ESCAPE);
      inputMap.put(KeyStroke.getKeyStroke("control ENTER"), ActionType.ACTIVATE_EDITIION);
    }
    
    if (OperatingSystem.isWindows()) {
      // Under Windows, magnetism toggled with Alt key 
      inputMap.put(KeyStroke.getKeyStroke("alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("released ALT"), ActionType.TOGGLE_MAGNETISM_OFF);
      inputMap.put(KeyStroke.getKeyStroke("shift alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("shift released ALT"), ActionType.TOGGLE_MAGNETISM_OFF);
      inputMap.put(KeyStroke.getKeyStroke("control alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("control released ALT"), ActionType.TOGGLE_MAGNETISM_OFF);
      inputMap.put(KeyStroke.getKeyStroke("shift control alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("shift control released ALT"), ActionType.TOGGLE_MAGNETISM_OFF);
      inputMap.put(KeyStroke.getKeyStroke("alt ESCAPE"), ActionType.ESCAPE);
      inputMap.put(KeyStroke.getKeyStroke("alt ENTER"), ActionType.ACTIVATE_EDITIION);
    } else if (OperatingSystem.isMacOSX()) {
      // Under Windows, magnetism toggled with cmd key 
      inputMap.put(KeyStroke.getKeyStroke("meta pressed META"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("released META"), ActionType.TOGGLE_MAGNETISM_OFF);
      inputMap.put(KeyStroke.getKeyStroke("shift meta pressed META"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("shift released META"), ActionType.TOGGLE_MAGNETISM_OFF);
      inputMap.put(KeyStroke.getKeyStroke("alt meta pressed META"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("alt released META"), ActionType.TOGGLE_MAGNETISM_OFF);
      inputMap.put(KeyStroke.getKeyStroke("shift alt meta pressed META"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("shift alt released META"), ActionType.TOGGLE_MAGNETISM_OFF);
      inputMap.put(KeyStroke.getKeyStroke("meta ESCAPE"), ActionType.ESCAPE);
      inputMap.put(KeyStroke.getKeyStroke("meta ENTER"), ActionType.ACTIVATE_EDITIION);
    } else {
      // Under other Unix systems, magnetism toggled with Alt + Shift key 
      inputMap.put(KeyStroke.getKeyStroke("shift alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("alt shift pressed SHIFT"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("alt released SHIFT"), ActionType.TOGGLE_MAGNETISM_OFF);
      inputMap.put(KeyStroke.getKeyStroke("control shift alt pressed ALT"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("control alt shift pressed SHIFT"), ActionType.TOGGLE_MAGNETISM_ON);
      inputMap.put(KeyStroke.getKeyStroke("control alt released SHIFT"), ActionType.TOGGLE_MAGNETISM_OFF);
      inputMap.put(KeyStroke.getKeyStroke("alt shift ESCAPE"), ActionType.ESCAPE);
      inputMap.put(KeyStroke.getKeyStroke("alt shift  ENTER"), ActionType.ACTIVATE_EDITIION);
      inputMap.put(KeyStroke.getKeyStroke("control alt shift ESCAPE"), ActionType.ESCAPE);
      inputMap.put(KeyStroke.getKeyStroke("control alt shift  ENTER"), ActionType.ACTIVATE_EDITIION);
    }

    inputMap.put(KeyStroke.getKeyStroke("shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
    inputMap.put(KeyStroke.getKeyStroke("released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
    if (OperatingSystem.isWindows()) {
      inputMap.put(KeyStroke.getKeyStroke("control shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
      inputMap.put(KeyStroke.getKeyStroke("control released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
      inputMap.put(KeyStroke.getKeyStroke("alt shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
      inputMap.put(KeyStroke.getKeyStroke("alt released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
      
    } else if (OperatingSystem.isMacOSX()) {
      inputMap.put(KeyStroke.getKeyStroke("alt shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
      inputMap.put(KeyStroke.getKeyStroke("alt released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
      inputMap.put(KeyStroke.getKeyStroke("meta shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
      inputMap.put(KeyStroke.getKeyStroke("meta released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
    } else {
      inputMap.put(KeyStroke.getKeyStroke("control shift pressed SHIFT"), ActionType.ACTIVATE_ALIGNMENT);
      inputMap.put(KeyStroke.getKeyStroke("control released SHIFT"), ActionType.DEACTIVATE_ALIGNMENT);
      inputMap.put(KeyStroke.getKeyStroke("shift released ALT"), ActionType.ACTIVATE_ALIGNMENT);
      inputMap.put(KeyStroke.getKeyStroke("control shift released ALT"), ActionType.ACTIVATE_ALIGNMENT);
    }
  }
  
  /**
   * Installs keys bound to actions during edition. 
   */
  private void installEditionKeyboardActions() {
    InputMap inputMap = getInputMap(WHEN_FOCUSED);
    inputMap.clear();
    inputMap.put(KeyStroke.getKeyStroke("ESCAPE"), ActionType.ESCAPE);
    inputMap.put(KeyStroke.getKeyStroke("shift ESCAPE"), ActionType.ESCAPE);
    inputMap.put(KeyStroke.getKeyStroke("ENTER"), ActionType.DEACTIVATE_EDITIION);
    inputMap.put(KeyStroke.getKeyStroke("shift ENTER"), ActionType.DEACTIVATE_EDITIION);
    if (OperatingSystem.isMacOSX()) {
      // Under Mac OS X, duplication with Alt key 
      inputMap.put(KeyStroke.getKeyStroke("alt ESCAPE"), ActionType.ESCAPE);
      inputMap.put(KeyStroke.getKeyStroke("alt ENTER"), ActionType.DEACTIVATE_EDITIION);
      inputMap.put(KeyStroke.getKeyStroke("alt shift ENTER"), ActionType.DEACTIVATE_EDITIION);
      inputMap.put(KeyStroke.getKeyStroke("alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("released ALT"), ActionType.DEACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift alt pressed ALT"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift released ALT"), ActionType.DEACTIVATE_DUPLICATION);
    } else {
      // Under other systems, duplication with Ctrl key 
      inputMap.put(KeyStroke.getKeyStroke("control ESCAPE"), ActionType.ESCAPE);
      inputMap.put(KeyStroke.getKeyStroke("control ENTER"), ActionType.DEACTIVATE_EDITIION);
      inputMap.put(KeyStroke.getKeyStroke("control shift ENTER"), ActionType.DEACTIVATE_EDITIION);
      inputMap.put(KeyStroke.getKeyStroke("control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift control pressed CONTROL"), ActionType.ACTIVATE_DUPLICATION);
      inputMap.put(KeyStroke.getKeyStroke("shift released CONTROL"), ActionType.DEACTIVATE_DUPLICATION);
    }
  }
 
  /**
   * Creates actions that calls back <code>controller</code> methods.  
   */
  private void createActions(final PlanController controller) {
    // Delete selection action 
    Action deleteSelectionAction = new AbstractAction() {
      public void actionPerformed(ActionEvent ev) {
        controller.deleteSelection();
      }
    };
    // Escape action 
    Action escapeAction = new AbstractAction() {
      public void actionPerformed(ActionEvent ev) {
        controller.escape();
      }
    };
    // Move selection action  
    class MoveSelectionAction extends AbstractAction {
      private final int dx;
      private final int dy;
      
      public MoveSelectionAction(int dx, int dy) {
        this.dx = dx;
        this.dy = dy;
      }

      public void actionPerformed(ActionEvent ev) {
        controller.moveSelection(this.dx / getScale(), this.dy / getScale());
      }
    }
    // Toggle magnetism action
    class ToggleMagnetismAction extends AbstractAction {
      private final boolean toggle;
      
      public ToggleMagnetismAction(boolean toggle) {
        this.toggle = toggle;
      }

      public void actionPerformed(ActionEvent ev) {
        controller.toggleMagnetism(this.toggle);
      }
    }
    // Alignment action
    class SetAlignmentActivatedAction extends AbstractAction {
      private final boolean alignmentActivated;
      
      public SetAlignmentActivatedAction(boolean alignmentActivated) {
        this.alignmentActivated = alignmentActivated;
      }
      
      public void actionPerformed(ActionEvent ev) {
        controller.setAlignmentActivated(this.alignmentActivated);
      }
    }
    // Duplication action
    class SetDuplicationActivatedAction extends AbstractAction {
      private final boolean duplicationActivated;
      
      public SetDuplicationActivatedAction(boolean duplicationActivated) {
        this.duplicationActivated = duplicationActivated;
      }

      public void actionPerformed(ActionEvent ev) {
        controller.setDuplicationActivated(this.duplicationActivated);
      }
    }
    // Edition action
    class SetEditionActivatedAction extends AbstractAction {
      private final boolean editionActivated;
      
      public SetEditionActivatedAction(boolean editionActivated) {
        this.editionActivated = editionActivated;
      }

      public void actionPerformed(ActionEvent ev) {
        controller.setEditionActivated(this.editionActivated);
      }
    }
    ActionMap actionMap = getActionMap();
    actionMap.put(ActionType.DELETE_SELECTION, deleteSelectionAction);
    actionMap.put(ActionType.ESCAPE, escapeAction);
    actionMap.put(ActionType.MOVE_SELECTION_LEFT, new MoveSelectionAction(-1, 0));
    actionMap.put(ActionType.MOVE_SELECTION_FAST_LEFT, new MoveSelectionAction(-10, 0));
    actionMap.put(ActionType.MOVE_SELECTION_UP, new MoveSelectionAction(0, -1));
    actionMap.put(ActionType.MOVE_SELECTION_FAST_UP, new MoveSelectionAction(0, -10));
    actionMap.put(ActionType.MOVE_SELECTION_DOWN, new MoveSelectionAction(0, 1));
    actionMap.put(ActionType.MOVE_SELECTION_FAST_DOWN, new MoveSelectionAction(0, 10));
    actionMap.put(ActionType.MOVE_SELECTION_RIGHT, new MoveSelectionAction(1, 0));
    actionMap.put(ActionType.MOVE_SELECTION_FAST_RIGHT, new MoveSelectionAction(10, 0));
    actionMap.put(ActionType.TOGGLE_MAGNETISM_ON, new ToggleMagnetismAction(true));
    actionMap.put(ActionType.TOGGLE_MAGNETISM_OFF, new ToggleMagnetismAction(false));
    actionMap.put(ActionType.ACTIVATE_ALIGNMENT, new SetAlignmentActivatedAction(true));
    actionMap.put(ActionType.DEACTIVATE_ALIGNMENT, new SetAlignmentActivatedAction(false));
    actionMap.put(ActionType.ACTIVATE_DUPLICATION, new SetDuplicationActivatedAction(true));
    actionMap.put(ActionType.DEACTIVATE_DUPLICATION, new SetDuplicationActivatedAction(false));
    actionMap.put(ActionType.ACTIVATE_EDITIION, new SetEditionActivatedAction(true));
    actionMap.put(ActionType.DEACTIVATE_EDITIION, new SetEditionActivatedAction(false));
  }

  /**
   * Creates the text fields used in tool tip and their label.
   */
  private void createToolTipTextFields(UserPreferences preferences, 
                                       final PlanController controller) {
    this.toolTipEditableTextFields = new HashMap<PlanController.EditableProperty, JFormattedTextField>();
    Font toolTipFont = UIManager.getFont("ToolTip.font");
    for (final PlanController.EditableProperty editableProperty : PlanController.EditableProperty.values()) {
      final JFormattedTextField textField = new JFormattedTextField() {
          @Override
          public Dimension getPreferredSize() {
            // Enlarge preferred size of one pixel
            Dimension preferredSize = super.getPreferredSize();
            return new Dimension(preferredSize.width + 1, preferredSize.height);
          }
        };
      updateToolTipTextFieldFormatterFactory(textField, editableProperty, preferences);
      textField.setFont(toolTipFont);
      textField.setOpaque(false);
      textField.setBorder(null);
      if (controller != null) {
        // Add a listener to notify changes to controller
        textField.getDocument().addDocumentListener(new DocumentListener() {
            public void changedUpdate(DocumentEvent ev) {
              try {
                textField.commitEdit();
                controller.updateEditableProperty(editableProperty, textField.getValue());
              } catch (ParseException ex) {
                controller.updateEditableProperty(editableProperty, null);
              }
            }
    
            public void insertUpdate(DocumentEvent ev) {
              changedUpdate(ev);
            }
    
            public void removeUpdate(DocumentEvent ev) {
              changedUpdate(ev);
            }
          });
      }

      this.toolTipEditableTextFields.put(editableProperty, textField);
    }
  }

  private static void updateToolTipTextFieldFormatterFactory(JFormattedTextField textField,
                                                             PlanController.EditableProperty editableProperty,
                                                             UserPreferences preferences) {
    InternationalFormatter formatter;
    if (editableProperty == PlanController.EditableProperty.ANGLE) {      
      formatter = new NumberFormatter(NumberFormat.getNumberInstance());
    } else {
      Format lengthFormat = preferences.getLengthUnit().getFormat();
      if (lengthFormat instanceof NumberFormat) {
        formatter = new NumberFormatter((NumberFormat)lengthFormat);
      } else {
        formatter = new InternationalFormatter(lengthFormat);
      }
    }
    textField.setFormatterFactory(new DefaultFormatterFactory(formatter));
  }
  
  /**
   * Returns a custom cursor with a hot spot point at center of cursor.
   */ 
  private Cursor createCustomCursor(String smallCursorImageResource, 
                                    String largeCursorImageResource,
                                    String cursorName,
                                    int    defaultCursor) {
    if (OperatingSystem.isMacOSX()) {
      smallCursorImageResource = smallCursorImageResource.replace(".png", "-macosx.png");
    }
    return createCustomCursor(PlanComponent.class.getResource(smallCursorImageResource), 
        PlanComponent.class.getResource(largeCursorImageResource), 
        0.5f, 0.5f, cursorName, 
        Cursor.getPredefinedCursor(defaultCursor));
  }

  /**
   * Returns a custom cursor created from images in parameters.
   */ 
  protected Cursor createCustomCursor(URL smallCursorImageUrl, 
                                      URL largeCursorImageUrl,
                                      float xCursorHotSpot,
                                      float yCursorHotSpot,
                                      String cursorName,
                                      Cursor defaultCursor) {
    return SwingTools.createCustomCursor(smallCursorImageUrl, largeCursorImageUrl, 
        xCursorHotSpot, yCursorHotSpot, cursorName, defaultCursor);
  }

  /**
   * Returns the preferred size of this component.
   */
  @Override
  public Dimension getPreferredSize() {
    if (isPreferredSizeSet()) {
      return super.getPreferredSize();
    } else {
      Insets insets = getInsets();
      Rectangle2D planBounds = getPlanBounds();
      return new Dimension(
          Math.round(((float)planBounds.getWidth() + MARGIN * 2)
                     * getScale()) + insets.left + insets.right,
          Math.round(((float)planBounds.getHeight() + MARGIN * 2)
                     * getScale()) + insets.top + insets.bottom);
    }
  }
  
  /**
   * Returns the bounds of the plan displayed by this component.
   */
  private Rectangle2D getPlanBounds() {
    if (!this.planBoundsCacheValid) {
      // Always enlarge plan bounds only when plan component is a child of a scroll pane 
      if (this.planBoundsCache == null 
          || !(getParent() instanceof JViewport)) {
        // Ensure plan bounds are 10 x 10 meters wide at minimum
        this.planBoundsCache = new Rectangle2D.Float(0, 0, 1000, 1000);
      }
      // Enlarge plan bounds to include background images, home bounds and observer camera
      if (this.backgroundImageCache != null) {
        BackgroundImage backgroundImage = this.home.getBackgroundImage();
        if (backgroundImage != null) {
          this.planBoundsCache.add(-backgroundImage.getXOrigin(), -backgroundImage.getYOrigin());
          this.planBoundsCache.add(this.backgroundImageCache.getWidth() * backgroundImage.getScale() - backgroundImage.getXOrigin(),
              this.backgroundImageCache.getHeight() * backgroundImage.getScale() - backgroundImage.getYOrigin());
        }
        for (Level level : this.home.getLevels()) {
          BackgroundImage levelBackgroundImage = level.getBackgroundImage();
          if (levelBackgroundImage != null) {
            this.planBoundsCache.add(-levelBackgroundImage.getXOrigin(), -levelBackgroundImage.getYOrigin());
            this.planBoundsCache.add(this.backgroundImageCache.getWidth() * levelBackgroundImage.getScale() - levelBackgroundImage.getXOrigin(),
                this.backgroundImageCache.getHeight() * levelBackgroundImage.getScale() - levelBackgroundImage.getYOrigin());
          }
        }
      }
      Rectangle2D homeItemsBounds = getItemsBounds(getGraphics(), getPaintedItems());
      if (homeItemsBounds != null) {
        this.planBoundsCache.add(homeItemsBounds);
      }
      for (float [] point : this.home.getObserverCamera().getPoints()) {
        this.planBoundsCache.add(point [0], point [1]);
      }
      this.planBoundsCacheValid = true;
    }
    return this.planBoundsCache;
  }
  
  /**
   * Returns the collection of walls, furniture, rooms and dimension lines of the home 
   * painted by this component wherever the level they belong to is selected or not.
   */
  protected List<Selectable> getPaintedItems() {
    List<Selectable> homeItems = new ArrayList<Selectable>(this.home.getWalls());
    for (HomePieceOfFurniture piece : this.home.getFurniture()) {
      if (piece.isVisible()) {
        homeItems.add(piece);
      }
    }
    homeItems.addAll(this.home.getRooms());
    homeItems.addAll(this.home.getDimensionLines());
    homeItems.addAll(this.home.getLabels());
    Compass compass = this.home.getCompass();
    if (compass.isVisible()) {
      homeItems.add(compass);
    }
    return homeItems;
  }
  
  /**
   * Returns the bounds of the given collection of <code>items</code>.
   */
  private Rectangle2D getItemsBounds(Graphics g, Collection<? extends Selectable> items) {
    Rectangle2D itemsBounds = null;
    for (Selectable item : items) {
      if (itemsBounds == null) {
        itemsBounds = getItemBounds(g, item);
      } else {
        itemsBounds.add(getItemBounds(g, item));
      }
    } 
    return itemsBounds;
  }
  
  /**
   * Returns the bounds of the given <code>item</code>.
   */
  protected Rectangle2D getItemBounds(Graphics g, Selectable item) {
    // Add to bounds all the visible items
    float [][] points = item.getPoints();
    Rectangle2D itemBounds = new Rectangle2D.Float(points [0][0], points [0][1], 0, 0);
    for (int i = 1; i < points.length; i++) {
      itemBounds.add(points [i][0], points [i][1]);
    }
    
    // Retrieve used font
    Font componentFont;
    if (g != null) {
      componentFont = g.getFont();
    } else {
      componentFont = getFont();
    }
    
    if (item instanceof Room) {
      // Add to bounds the displayed name and area bounds of each room 
      Room room = (Room)item;
      float xRoomCenter = room.getXCenter();
      float yRoomCenter = room.getYCenter();
      String roomName = room.getName();
      if (roomName != null && roomName.length() > 0) {
        addTextBounds(room.getClass(), 
            roomName, room.getNameStyle(), 
            xRoomCenter + room.getNameXOffset(), 
            yRoomCenter + room.getNameYOffset(), room.getNameAngle(), itemBounds);
      }
      if (room.isAreaVisible()) {
        float area = room.getArea();
        if (area > 0.01f) {
          String areaText = this.preferences.getLengthUnit().getAreaFormatWithUnit().format(area);
          addTextBounds(room.getClass(), 
              areaText, room.getAreaStyle(), 
              xRoomCenter + room.getAreaXOffset(), 
              yRoomCenter + room.getAreaYOffset(), room.getAreaAngle(), itemBounds);
        }
      }
    } else if (item instanceof HomePieceOfFurniture) {
      if (item instanceof HomeDoorOrWindow) {
        HomeDoorOrWindow doorOrWindow = (HomeDoorOrWindow)item;
        // Add to bounds door and window sashes 
        for (Sash sash : doorOrWindow.getSashes()) {
          itemBounds.add(getDoorOrWindowSashShape(doorOrWindow, sash).getBounds2D());
        }
      } else if (item instanceof HomeFurnitureGroup) {
        itemBounds.add(getItemsBounds(g, ((HomeFurnitureGroup)item).getFurniture()));
      }
      // Add to bounds the displayed name of the piece of furniture 
      HomePieceOfFurniture piece = (HomePieceOfFurniture)item;
      String pieceName = piece.getName();
      if (piece.isVisible()
          && piece.isNameVisible()
          && pieceName.length() > 0) {
        addTextBounds(piece.getClass(), 
            pieceName, piece.getNameStyle(), 
            piece.getX() + piece.getNameXOffset(), 
            piece.getY() + piece.getNameYOffset(), piece.getNameAngle(), itemBounds);
      }
    } else if (item instanceof DimensionLine) {
      // Add to bounds the text bounds of the dimension line length 
      DimensionLine dimensionLine = (DimensionLine)item;
      float dimensionLineLength = dimensionLine.getLength();
      String lengthText = this.preferences.getLengthUnit().getFormat().format(dimensionLineLength);
      TextStyle lengthStyle = dimensionLine.getLengthStyle();
      if (lengthStyle == null) {
        lengthStyle = this.preferences.getDefaultTextStyle(dimensionLine.getClass());
      }          
      FontMetrics lengthFontMetrics = getFontMetrics(componentFont, lengthStyle);
      Rectangle2D lengthTextBounds = lengthFontMetrics.getStringBounds(lengthText, g);
      // Transform length text bounding rectangle corners to their real location
      double angle = Math.atan2(dimensionLine.getYEnd() - dimensionLine.getYStart(), 
          dimensionLine.getXEnd() - dimensionLine.getXStart());
      AffineTransform transform = AffineTransform.getTranslateInstance(
          dimensionLine.getXStart(), dimensionLine.getYStart());
      transform.rotate(angle);
      transform.translate(0, dimensionLine.getOffset());
      transform.translate((dimensionLineLength - lengthTextBounds.getWidth()) / 2, 
          dimensionLine.getOffset() <= 0 
              ? -lengthFontMetrics.getDescent() - 1
              : lengthFontMetrics.getAscent() + 1);
      GeneralPath lengthTextBoundsPath = new GeneralPath(lengthTextBounds);
      for (PathIterator it = lengthTextBoundsPath.getPathIterator(transform); !it.isDone(); ) {
        float [] pathPoint = new float[2];
        if (it.currentSegment(pathPoint) != PathIterator.SEG_CLOSE) {
          itemBounds.add(pathPoint [0], pathPoint [1]);
        }
        it.next();
      }
      // Add to bounds the end lines drawn at dimension line start and end  
      transform.setToTranslation(dimensionLine.getXStart(), dimensionLine.getYStart());
      transform.rotate(angle);
      transform.translate(0, dimensionLine.getOffset());
      for (PathIterator it = DIMENSION_LINE_END.getPathIterator(transform); !it.isDone(); ) {
        float [] pathPoint = new float[2];
        if (it.currentSegment(pathPoint) != PathIterator.SEG_CLOSE) {
          itemBounds.add(pathPoint [0], pathPoint [1]);
        }
        it.next();
      }
      transform.translate(dimensionLineLength, 0);
      for (PathIterator it = DIMENSION_LINE_END.getPathIterator(transform); !it.isDone(); ) {
        float [] pathPoint = new float[2];
        if (it.currentSegment(pathPoint) != PathIterator.SEG_CLOSE) {
          itemBounds.add(pathPoint [0], pathPoint [1]);
        }
        it.next();
      }
    } else if (item instanceof Label) {
      // Add to bounds the displayed text of a label 
      Label label = (Label)item;
      addTextBounds(label.getClass(), 
          label.getText(), label.getStyle(), label.getX(), label.getY(), label.getAngle(), itemBounds);
    } else if (item instanceof Compass) {
      Compass compass = (Compass)item;
      AffineTransform transform = AffineTransform.getTranslateInstance(compass.getX(), compass.getY());
      transform.scale(compass.getDiameter(), compass.getDiameter());
      transform.rotate(compass.getNorthDirection());
      return COMPASS.createTransformedShape(transform).getBounds2D();
    } 
    return itemBounds;
  }
  
  /**
   * Add <code>text</code> bounds to the given rectangle <code>bounds</code>.
   */
  private void addTextBounds(Class<? extends Selectable> selectableClass,
                             String text, TextStyle style, 
                             float x, float y, float angle,
                             Rectangle2D bounds) {
    if (style == null) {
      style = this.preferences.getDefaultTextStyle(selectableClass);
    }          
    for (float [] points : getTextBounds(text, style, x, y, angle)) {
      bounds.add(points [0], points [1]);
    }
  }
  
  /**
   * Returns the coordinates of the bounding rectangle of the <code>text</code> centered at
   * the point (<code>x</code>,<code>y</code>).  
   */
  public float [][] getTextBounds(String text, TextStyle style, 
                                  float x, float y, float angle) {
    FontMetrics fontMetrics = getFontMetrics(getFont(), style);
    Rectangle2D textBounds = fontMetrics.getStringBounds(text, null);
    float halfTextLength = (float)textBounds.getWidth() / 2;
    if (angle == 0) {
      float minY = (float)(y + textBounds.getY());
      float maxY = (float)(minY + textBounds.getHeight());
      return new float [][] {
          {x - halfTextLength, minY},
          {x + halfTextLength, minY},
          {x + halfTextLength, maxY},
          {x - halfTextLength, maxY}};
    } else {
      // Transform text bounding rectangle corners to their real location
      AffineTransform transform = new AffineTransform();
      transform.translate(x, y);
      transform.rotate(angle);
      transform.translate(-halfTextLength, 0);
      GeneralPath textBoundsPath = new GeneralPath(textBounds);
      List<float []> textPoints = new ArrayList<float[]>(4);
      for (PathIterator it = textBoundsPath.getPathIterator(transform); !it.isDone(); ) {
        float [] pathPoint = new float[2];
        if (it.currentSegment(pathPoint) != PathIterator.SEG_CLOSE) {
          textPoints.add(pathPoint);
        }
        it.next();
      }
      return textPoints.toArray(new float [textPoints.size()][]);
    }
  }
  
  /**
   * Returns the AWT font matching a given text style.
   */
  protected Font getFont(Font defaultFont, TextStyle textStyle) {
    if (this.fonts == null) {
      this.fonts = new WeakHashMap<TextStyle, Font>();
    }
    Font font = this.fonts.get(textStyle);
    if (font == null) {
      int fontStyle = Font.PLAIN;
      if (textStyle.isBold()) {
        fontStyle = Font.BOLD;
      }
      if (textStyle.isItalic()) {
        fontStyle |= Font.ITALIC;
      }
      String fontName = null;
      if (defaultFont != null) {
        fontName = defaultFont.getName();
      } else {
        fontName = null;
      }
      font = new Font(fontName, fontStyle, 1);
      font = font.deriveFont(textStyle.getFontSize());
      this.fonts.put(textStyle, font);
    }
    return font;
  }

  /**
   * Returns the font metrics matching a given text style.
   */
  protected FontMetrics getFontMetrics(Font defaultFont, TextStyle textStyle) {
    if (this.fontsMetrics == null) {
      this.fontsMetrics = new WeakHashMap<TextStyle, FontMetrics>();
    }
    FontMetrics fontMetrics = this.fontsMetrics.get(textStyle);
    if (fontMetrics == null) {
      fontMetrics = getFontMetrics(getFont(defaultFont, textStyle));
      this.fontsMetrics.put(textStyle, fontMetrics);
    }
    return fontMetrics;
  }

  /**
   * Sets whether plan's background should be painted or not.
   * Background may include grid and an image.   
   */
  public void setBackgroundPainted(boolean backgroundPainted) {
    if (this.backgroundPainted != backgroundPainted) {
      this.backgroundPainted = backgroundPainted;
      repaint();
    }
  }
  
  /**
   * Returns <code>true</code> if plan's background should be painted.
   */
  public boolean isBackgroundPainted() {
    return this.backgroundPainted;
  }

  /**
   * Sets whether the outline of home selected items should be painted or not.
   */
  public void setSelectedItemsOutlinePainted(boolean selectedItemsOutlinePainted) {
    if (this.selectedItemsOutlinePainted != selectedItemsOutlinePainted) {
      this.selectedItemsOutlinePainted = selectedItemsOutlinePainted;
      repaint();
    }
  }
  
  /**
   * Returns <code>true</code> if the outline of home selected items should be painted.
   */
  public boolean isSelectedItemsOutlinePainted() {
    return this.selectedItemsOutlinePainted;
  }
  
  /**
   * Paints this component.
   */
  @Override
  protected void paintComponent(Graphics g) {
    Graphics2D g2D = (Graphics2D)g.create();
    if (this.backgroundPainted) {
      paintBackground(g2D, getBackgroundColor(PaintMode.PAINT));
    }
    Insets insets = getInsets();
    // Clip component to avoid drawing in empty borders
    g2D.clipRect(insets.left, insets.top, 
        getWidth() - insets.left - insets.right, 
        getHeight() - insets.top - insets.bottom);
    // Change component coordinates system to plan system
    Rectangle2D planBounds = getPlanBounds();    
    float paintScale = getScale();
    g2D.translate(insets.left + (MARGIN - planBounds.getMinX()) * paintScale,
        insets.top + (MARGIN - planBounds.getMinY()) * paintScale);
    g2D.scale(paintScale, paintScale);
    setRenderingHints(g2D);
    try {
      paintContent(g2D, paintScale, PaintMode.PAINT);
    } catch (InterruptedIOException ex) {
      // Ignore exception because it may happen only in EXPORT paint mode 
    }   
    g2D.dispose();
  }

  /**
   * Returns the print preferred scale of the plan drawn in this component
   * to make it fill <code>pageFormat</code> imageable size.
   */
  public float getPrintPreferredScale(Graphics g, PageFormat pageFormat) {
    List<Selectable> printedItems = getPaintedItems(); 
    Rectangle2D printedItemBounds = getItemsBounds(g, printedItems);
    if (printedItemBounds != null) {
      float imageableWidthCm = LengthUnit.inchToCentimeter((float)pageFormat.getImageableWidth() / 72);
      float imageableHeightCm = LengthUnit.inchToCentimeter((float)pageFormat.getImageableHeight() / 72);
      float extraMargin = getStrokeWidthExtraMargin(printedItems, PaintMode.PRINT);
      // Compute the largest integer scale possible
      int scaleInverse = (int)Math.ceil(Math.max(
          (printedItemBounds.getWidth() + 2 * extraMargin) / imageableWidthCm,
          (printedItemBounds.getHeight() + 2 * extraMargin) / imageableHeightCm));
      return 1f / scaleInverse;
    } else {
      return 0;
    }
  }
  
  /**
   * Returns the margin that should be added around home items bounds to ensure their
   * line stroke width is always fully visible.
   */
  private float getStrokeWidthExtraMargin(List<Selectable> items, PaintMode paintMode) {
    float extraMargin = BORDER_STROKE_WIDTH;
    if (Home.getFurnitureSubList(items).size() > 0) {
      extraMargin = Math.max(extraMargin, getStrokeWidth(HomePieceOfFurniture.class, paintMode));
    } 
    if (Home.getWallsSubList(items).size() > 0) {
      extraMargin = Math.max(extraMargin, getStrokeWidth(Wall.class, paintMode));
    } 
    if (Home.getRoomsSubList(items).size() > 0) {
      extraMargin = Math.max(extraMargin, getStrokeWidth(Room.class, paintMode));
    }
    if (Home.getDimensionLinesSubList(items).size() > 0) {
      extraMargin = Math.max(extraMargin, getStrokeWidth(DimensionLine.class, paintMode));
    }
    return extraMargin / 2;
  }
  
  /**
   * Returns the stroke width used to paint an item of the given class.
   */
  private float getStrokeWidth(Class<? extends Selectable> itemClass, PaintMode paintMode) {
    float strokeWidth; 
    if (Wall.class.isAssignableFrom(itemClass)
        || Room.class.isAssignableFrom(itemClass)) {
      strokeWidth = WALL_STROKE_WIDTH;
    } else {
      strokeWidth = BORDER_STROKE_WIDTH;
    }
    if (paintMode == PaintMode.PRINT) {
      strokeWidth *= 0.5;
    }
    return strokeWidth;
  }
  
  /**
   * Prints this component plan at the scale given in the home print attributes or at a scale 
   * that makes it fill <code>pageFormat</code> imageable size if this attribute is <code>null</code>.
   */
  public int print(Graphics g, PageFormat pageFormat, int pageIndex) {
    List<Selectable> printedItems = getPaintedItems(); 
    Rectangle2D printedItemBounds = getItemsBounds(g, printedItems);
    if (printedItemBounds != null) {
      double imageableX = pageFormat.getImageableX();
      double imageableY = pageFormat.getImageableY();
      double imageableWidth = pageFormat.getImageableWidth();
      double imageableHeight = pageFormat.getImageableHeight();
      float printScale;
      float rowIndex;
      float columnIndex;
      int pagesPerRow;
      int pagesPerColumn;
      if (this.home.getPrint() == null || this.home.getPrint().getPlanScale() == null) {
        // Compute a scale that ensures the plan will fill the component if plan scale is null
        printScale = getPrintPreferredScale(g, pageFormat) * LengthUnit.centimeterToInch(72);
        if (pageIndex > 0) {
          return NO_SUCH_PAGE;
        }
        pagesPerRow = 1;
        pagesPerColumn = 1;
        rowIndex   = 0;
        columnIndex = 0;
      } else {
        // Apply print scale to paper size expressed in 1/72nds of an inch
        printScale = this.home.getPrint().getPlanScale().floatValue() * LengthUnit.centimeterToInch(72);
        pagesPerRow = (int)(printedItemBounds.getWidth() * printScale / imageableWidth);
        if (printedItemBounds.getWidth() * printScale != imageableWidth) {
          pagesPerRow++;
        }
        pagesPerColumn = (int)(printedItemBounds.getHeight() * printScale / imageableHeight);
        if (printedItemBounds.getHeight() * printScale != imageableHeight) {
          pagesPerColumn++;
        }
        if (pageIndex >= pagesPerRow * pagesPerColumn) {
          return NO_SUCH_PAGE;
        }
        rowIndex = pageIndex / pagesPerRow;
        columnIndex = pageIndex - rowIndex * pagesPerRow;
      }
          
      Graphics2D g2D = (Graphics2D)g.create();
      g2D.clip(new Rectangle2D.Double(imageableX, imageableY, imageableWidth, imageableHeight));
      // Change coordinates system to paper imageable origin
      g2D.translate(imageableX - columnIndex * imageableWidth, imageableY - rowIndex * imageableHeight);
      g2D.scale(printScale, printScale);
      float extraMargin = getStrokeWidthExtraMargin(printedItems, PaintMode.PRINT);
      g2D.translate(-printedItemBounds.getMinX() + extraMargin,
          -printedItemBounds.getMinY() + extraMargin);
      // Center plan in component if possible
      g2D.translate(Math.max(0, 
              (imageableWidth * pagesPerRow / printScale - printedItemBounds.getWidth() - 2 * extraMargin) / 2), 
          Math.max(0, 
              (imageableHeight * pagesPerColumn / printScale - printedItemBounds.getHeight() - 2 * extraMargin) / 2));
      setRenderingHints(g2D);
      try {
        // Print component contents
        paintContent(g2D, printScale, PaintMode.PRINT);
      } catch (InterruptedIOException ex) {
        // Ignore exception because it may happen only in EXPORT paint mode 
      }   
      g2D.dispose();
      return PAGE_EXISTS;
    } else {
      return NO_SUCH_PAGE;
    }
  }
  
  /**
   * Returns an image of the selected items displayed by this component 
   * (camera excepted) with no outline at scale 1/1 (1 pixel = 1cm).
   */
  public BufferedImage getClipboardImage() {
    // Create an image that contains only selected items
    Rectangle2D selectionBounds = getSelectionBounds(false);
    if (selectionBounds == null) {
      return null;
    } else {
      // Use a scale of 1
      float clipboardScale = 1f;
      float extraMargin = getStrokeWidthExtraMargin(this.home.getSelectedItems(), PaintMode.CLIPBOARD);
      BufferedImage image = new BufferedImage((int)Math.ceil(selectionBounds.getWidth() * clipboardScale + 2 * extraMargin), 
              (int)Math.ceil(selectionBounds.getHeight() * clipboardScale + 2 * extraMargin), BufferedImage.TYPE_INT_RGB);      
      Graphics2D g2D = (Graphics2D)image.getGraphics();
      // Paint background in white
      g2D.setColor(Color.WHITE);
      g2D.fillRect(0, 0, image.getWidth(), image.getHeight());
      // Change component coordinates system to plan system
      g2D.scale(clipboardScale, clipboardScale);
      g2D.translate(-selectionBounds.getMinX() + extraMargin,
          -selectionBounds.getMinY() + extraMargin);
      setRenderingHints(g2D);
      try {
        // Paint component contents
        paintContent(g2D, clipboardScale, PaintMode.CLIPBOARD);
      } catch (InterruptedIOException ex) {
        // Ignore exception because it may happen only in EXPORT paint mode 
      }   
      g2D.dispose();
      return image;
    }
  }
  
  /**
   * Writes this plan in the given output stream at SVG (Scalable Vector Graphics) format.
   */
  public void exportToSVG(OutputStream outputStream) throws IOException {
    SVGSupport.exportToSVG(outputStream, this);   
  }
  
  /**
   * Separated static class to be able to exclude FreeHEP library from classpath
   * in case the application doesn't use export to SVG format. 
   */
  private static class SVGSupport {
    public static void exportToSVG(OutputStream outputStream, 
                                   PlanComponent planComponent) throws IOException {
      List<Selectable> homeItems = planComponent.getPaintedItems();
      Rectangle2D svgItemBounds = planComponent.getItemsBounds(null, homeItems);
      if (svgItemBounds == null) {
        svgItemBounds = new Rectangle2D.Float();
      }
      
      float svgScale = 1f;
      float extraMargin = planComponent.getStrokeWidthExtraMargin(homeItems, PaintMode.EXPORT);
      Dimension imageSize = new Dimension((int)Math.ceil(svgItemBounds.getWidth() * svgScale + 2 * extraMargin), 
          (int)Math.ceil(svgItemBounds.getHeight() * svgScale + 2 * extraMargin));
      
      SVGGraphics2D exportG2D = new SVGGraphics2D(outputStream, imageSize) {
          @Override
          public void writeHeader() throws IOException {
            // Use English locale to avoid wrong encoding when localized dates contain accentuated letters 
            Locale defaultLocale = Locale.getDefault();
            Locale.setDefault(Locale.ENGLISH);
            super.writeHeader();
            Locale.setDefault(defaultLocale);
          }
        };
      UserProperties properties = new UserProperties();
      properties.setProperty(SVGGraphics2D.STYLABLE, true);
      properties.setProperty(SVGGraphics2D.WRITE_IMAGES_AS, ImageConstants.PNG);
      properties.setProperty(SVGGraphics2D.TITLE, 
          planComponent.home.getName() != null 
              ? planComponent.home.getName() 
              : "" );
      properties.setProperty(SVGGraphics2D.FOR, System.getProperty("user.name", ""));
      exportG2D.setProperties(properties);
      exportG2D.startExport();
      exportG2D.translate(-svgItemBounds.getMinX() + extraMargin,
          -svgItemBounds.getMinY() + extraMargin);
      
      planComponent.checkCurrentThreadIsntInterrupted(PaintMode.EXPORT);
      planComponent.paintContent(exportG2D, svgScale, PaintMode.EXPORT);   
      exportG2D.endExport();
    }
  }
  
  /**
   * Throws an <code>InterruptedRecorderException</code> exception if current thread 
   * is interrupted and <code>paintMode</code> is equal to <code>PaintMode.EXPORT</code>.  
   */
  private void checkCurrentThreadIsntInterrupted(PaintMode paintMode) throws InterruptedIOException {
    if (paintMode == PaintMode.EXPORT
        && Thread.interrupted()) {
      throw new InterruptedIOException("Current thread interrupted");
    }
  }
  
  /**
   * Sets rendering hints used to paint plan.
   */
  private void setRenderingHints(Graphics2D g2D) {
    g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
    g2D.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
  }

  /**
   * Fills the background. 
   */
  private void paintBackground(Graphics2D g2D, Color backgroundColor) {
    if (isOpaque()) {
      g2D.setColor(backgroundColor);
      g2D.fillRect(0, 0, getWidth(), getHeight());
    }
  }

  /**
   * Paints background image and returns <code>true</code> if an image is painted.
   */
  private boolean paintBackgroundImage(Graphics2D g2D, PaintMode paintMode) {
    final BackgroundImage backgroundImage = this.home.getSelectedLevel() == null 
        ? this.home.getBackgroundImage()
        : this.home.getSelectedLevel().getBackgroundImage();
    if (backgroundImage != null && backgroundImage.isVisible()) {
      if (this.backgroundImageCache == null && paintMode == PaintMode.PAINT) {
        // Load background image in an executor
        Executors.newSingleThreadExecutor().execute(new Runnable() {
            public void run() {
              backgroundImageCache = readImage(backgroundImage.getImage()); 
              revalidate();
            }
          });
      } else {
        // Paint image at specified scale with 0.7 alpha
        AffineTransform previousTransform = g2D.getTransform();
        g2D.translate(-backgroundImage.getXOrigin(), -backgroundImage.getYOrigin());
        g2D.scale(backgroundImage.getScale(), backgroundImage.getScale());
        Composite oldComposite = setTransparency(g2D, 0.7f);
        g2D.drawImage(this.backgroundImageCache != null
            ? this.backgroundImageCache
            : readImage(backgroundImage.getImage()), 0, 0, this);
        g2D.setComposite(oldComposite);
        g2D.setTransform(previousTransform);
      }
      return true;
    } else {
      return false;
    }
  }

  /**
   * Returns the foreground color used to draw content. 
   */
  protected Color getForegroundColor(PaintMode mode) {
    if (mode == PaintMode.PAINT) {
      return getForeground();
    } else {
      return Color.BLACK;
    }
  }

  /**
   * Returns the background color used to draw content. 
   */
  protected Color getBackgroundColor(PaintMode mode) {
    if (mode == PaintMode.PAINT) {
      return getBackground();
    } else {
      return Color.WHITE;
    }
  }

  /**
   * Returns the image contained in <code>imageContent</code> or an empty image if reading failed.
   */
  private BufferedImage readImage(Content imageContent) {
    InputStream contentStream = null;
    try {
      try {
        contentStream = imageContent.openStream();
        return ImageIO.read(contentStream);
      } finally {
        contentStream.close();
      }
    } catch (IOException ex) {
      return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
      // Ignore exceptions, the user may know its background image is incorrect 
      // if he tries to modify the background image
    }
  } 

  /**
   * Paints walls and rooms of lower levels or upper levels to help the user draw in the selected level. 
   */
  private void paintOtherLevels(Graphics2D g2D, float planScale, 
                                Color backgroundColor, Color foregroundColor) {
    List<Level> levels = this.home.getLevels();
    Level selectedLevel = this.home.getSelectedLevel();
    if (levels.size() > 1 
        && selectedLevel != null) {
      int selectedLevelIndex = levels.indexOf(selectedLevel);
      boolean level0 = levels.get(0) == selectedLevel 
          || levels.get(selectedLevelIndex - 1).getElevation() == selectedLevel.getElevation();
      if (this.otherLevelRoomAreaCache == null
          && (!level0 || levels.size() >= 2)) {
        // Search floors in level 1 or ceilings in previous level 
        Level otherLevel = levels.get(level0 && selectedLevelIndex < levels.size() - 1 
            ? selectedLevelIndex + 1 
            : selectedLevelIndex  - 1);
        List<Room> otherLevelRooms = new ArrayList<Room>();
        for (Room room : this.home.getRooms()) {
          if (room.isAtLevel(otherLevel)
              && (level0 && room.isFloorVisible()
                  || !level0 && room.isCeilingVisible())) {
            otherLevelRooms.add(room);
          }
        }
        if (otherLevelRooms.size() > 0) {
          this.otherLevelRoomAreaCache = getItemsArea(otherLevelRooms);
          this.otherLevelRoomsCache = otherLevelRooms;
        }
      }
      if (this.otherLevelRoomAreaCache != null) {
        Composite oldComposite = setTransparency(g2D, 
            this.preferences.isGridVisible() ? 0.2f : 0.1f);
        g2D.setPaint(Color.GRAY);
        g2D.fill(this.otherLevelRoomAreaCache);
        g2D.setComposite(oldComposite);
      }

      if (this.otherLevelWallAreaCache == null) {
        // Search visible walls in level 1 or in previous level 
        Level otherLevel = levels.get(level0 && selectedLevelIndex < levels.size() - 1 
            ? selectedLevelIndex + 1 
            : selectedLevelIndex  - 1);
        List<Wall> otherLevelwalls = new ArrayList<Wall>();
        for (Wall wall : this.home.getWalls()) {
          if (!isViewableAtSelectedLevel(wall)
              && wall.isAtLevel(otherLevel)) {
            otherLevelwalls.add(wall);
          }
        }
        if (otherLevelwalls.size() > 0) {
          this.otherLevelWallAreaCache = getItemsArea(otherLevelwalls);
          this.otherLevelWallsCache = otherLevelwalls;
        }
      }
      if (this.otherLevelWallAreaCache != null) {
        Composite oldComposite = setTransparency(g2D, 
            this.preferences.isGridVisible() ? 0.2f : 0.1f);
        fillAndDrawWallsArea(g2D, this.otherLevelWallAreaCache, planScale, 
            getWallPaint(planScale, backgroundColor, foregroundColor, this.preferences.getWallPattern()), 
            foregroundColor, PaintMode.PAINT);
        g2D.setComposite(oldComposite);
      }
    }
  }

  /**
   * Sets the transparency composite to the given percentage and returns the old composite.
   */
  private Composite setTransparency(Graphics2D g2D, float alpha) {
    Composite oldComposite = g2D.getComposite();
    if (oldComposite instanceof AlphaComposite) {
      g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
        ((AlphaComposite)oldComposite).getAlpha() * alpha));
    } else {
      g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
    }
    return oldComposite;
  }

  /**
   * Paints background grid lines.
   */
  private void paintGrid(Graphics2D g2D, float gridScale) {
    float gridSize = getGridSize(gridScale);
    float mainGridSize = getMainGridSize(gridScale);
    
    float xMin;
    float yMin;
    float xMax;
    float yMax;
    Rectangle2D planBounds = getPlanBounds();    
    if (getParent() instanceof JViewport) {
      Rectangle viewRectangle = ((JViewport)getParent()).getViewRect();
      xMin = convertXPixelToModel(viewRectangle.x - 1);
      yMin = convertYPixelToModel(viewRectangle.y - 1);
      xMax = convertXPixelToModel(viewRectangle.x + viewRectangle.width);
      yMax = convertYPixelToModel(viewRectangle.y + viewRectangle.height);
    } else {
      xMin = (float)planBounds.getMinX() - MARGIN;
      yMin = (float)planBounds.getMinY() - MARGIN;
      xMax = convertXPixelToModel(getWidth());
      yMax = convertYPixelToModel(getHeight());
    }
    boolean useGridImage = false;
    try {
      useGridImage = OperatingSystem.isMacOSX()
          && System.getProperty("apple.awt.graphics.UseQuartz", "false").equals("false");
    } catch (AccessControlException ex) {
      // Unsigned applet
    }
    if (useGridImage) {
      // Draw grid with an image texture under Mac OS X, because default 2D rendering engine 
      // is too slow and can't be replaced by Quartz engine in applet environment 
      int imageWidth = Math.round(mainGridSize * gridScale);
      BufferedImage gridImage = new BufferedImage(imageWidth, imageWidth, BufferedImage.TYPE_INT_ARGB);
      Graphics2D imageGraphics = (Graphics2D)gridImage.getGraphics();
      setRenderingHints(imageGraphics);
      imageGraphics.scale(gridScale, gridScale);
      
      paintGridLines(imageGraphics, gridScale, 0, mainGridSize, 0, mainGridSize, gridSize, mainGridSize);    
      imageGraphics.dispose();
      
      g2D.setPaint(new TexturePaint(gridImage, new Rectangle2D.Float(0, 0, mainGridSize, mainGridSize)));
      
      g2D.fill(new Rectangle2D.Float(xMin, yMin, xMax - xMin, yMax - yMin));
    } else {
      paintGridLines(g2D, gridScale, xMin, xMax, yMin, yMax, gridSize, mainGridSize);          
    }
  }

  /**
   * Paints background grid lines from <code>xMin</code> to <code>xMax</code> 
   * and <code>yMin</code> to <code>yMax</code>.
   */
  private void paintGridLines(Graphics2D g2D, float gridScale, 
                              float xMin, float xMax, float yMin, float yMax, 
                              float gridSize, float mainGridSize) {
    g2D.setColor(UIManager.getColor("controlShadow"));
    g2D.setStroke(new BasicStroke(0.5f / gridScale));
    // Draw vertical lines
    for (double x = (int)(xMin / gridSize) * gridSize; x < xMax; x += gridSize) {
      g2D.draw(new Line2D.Double(x, yMin, x, yMax));
    }
    // Draw horizontal lines
    for (double y = (int)(yMin / gridSize) * gridSize; y < yMax; y += gridSize) {
      g2D.draw(new Line2D.Double(xMin, y, xMax, y));
    }

    if (mainGridSize != gridSize) {
      g2D.setStroke(new BasicStroke(1.5f / gridScale,
          BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
      // Draw main vertical lines
      for (double x = (int)(xMin / mainGridSize) * mainGridSize; x < xMax; x += mainGridSize) {
        g2D.draw(new Line2D.Double(x, yMin, x, yMax));
      }
      // Draw positive main horizontal lines
      for (double y = (int)(yMin / mainGridSize) * mainGridSize; y < yMax; y += mainGridSize) {
        g2D.draw(new Line2D.Double(xMin, y, xMax, y));
      }
    }
  }

  /**
   * Returns the space between main lines grid.
   */
  private float getMainGridSize(float gridScale) {
    float [] mainGridSizes;
    LengthUnit lengthUnit = this.preferences.getLengthUnit();
    if (lengthUnit == LengthUnit.INCH 
        || lengthUnit == LengthUnit.INCH_DECIMALS) {
      // Use a grid in inch and foot with a minimum grid increment of 1 inch
      float oneFoot = 2.54f * 12;
      mainGridSizes = new float [] {oneFoot, 3 * oneFoot, 6 * oneFoot, 
                                    12 * oneFoot, 24 * oneFoot, 48 * oneFoot, 96 * oneFoot, 192 * oneFoot, 384 * oneFoot}; 
    } else {
      // Use a grid in cm and meters with a minimum grid increment of 1 cm
      mainGridSizes = new float [] {100, 200, 500, 1000, 2000, 5000, 10000};
    }
    // Compute grid size to get a grid where the space between each line is less than 50 pixels
    float mainGridSize = mainGridSizes [0];
    for (int i = 1; i < mainGridSizes.length && mainGridSize * gridScale < 50; i++) {
      mainGridSize = mainGridSizes [i];
    }
    return mainGridSize;
  }
  
  /**
   * Returns the space between lines grid.
   */
  private float getGridSize(float gridScale) {
    float [] gridSizes;
    LengthUnit lengthUnit = this.preferences.getLengthUnit();
    if (lengthUnit == LengthUnit.INCH 
        || lengthUnit == LengthUnit.INCH_DECIMALS) {
      // Use a grid in inch and foot with a minimum grid increment of 1 inch
      float oneFoot = 2.54f * 12;
      gridSizes = new float [] {2.54f, 5.08f, 7.62f, 15.24f, oneFoot, 3 * oneFoot, 6 * oneFoot, 
                                12 * oneFoot, 24 * oneFoot, 48 * oneFoot, 96 * oneFoot, 192 * oneFoot, 384 * oneFoot};
    } else {
      // Use a grid in cm and meters with a minimum grid increment of 1 cm
      gridSizes = new float [] {1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000};
    }
    // Compute grid size to get a grid where the space between each line is less than 10 pixels
    float gridSize = gridSizes [0];
    for (int i = 1; i < gridSizes.length && gridSize * gridScale < 10; i++) {
      gridSize = gridSizes [i];
    }
    return gridSize;
  }
  
  /**
   * Paints plan items.
   * @throws InterruptedIOException if painting was interrupted (may happen only 
   *           if <code>paintMode</code> is equal to <code>PaintMode.EXPORT</code>).
   */
  private void paintContent(Graphics2D g2D, float planScale, PaintMode paintMode) throws InterruptedIOException {
    Color backgroundColor = getBackgroundColor(paintMode);
    Color foregroundColor = getForegroundColor(paintMode);
    if (this.backgroundPainted) {
      paintBackgroundImage(g2D, paintMode);
      if (paintMode == PaintMode.PAINT) {
        paintOtherLevels(g2D, planScale, backgroundColor, foregroundColor);
        if (this.preferences.isGridVisible()) {
          paintGrid(g2D, planScale);
        }
      }
    }
    
    paintHomeItems(g2D, planScale, backgroundColor, foregroundColor, paintMode);
        
    if (paintMode == PaintMode.PAINT) {
      List<Selectable> selectedItems = this.home.getSelectedItems();
      
      Color selectionColor = getSelectionColor(); 
      Color furnitureOutlineColor = getFurnitureOutlineColor();
      Paint selectionOutlinePaint = new Color(selectionColor.getRed(), selectionColor.getGreen(), 
          selectionColor.getBlue(), 128);
      Stroke selectionOutlineStroke = new BasicStroke(6 / planScale, 
          BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); 
      Stroke dimensionLinesSelectionOutlineStroke = new BasicStroke(4 / planScale, 
          BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); 
      Stroke locationFeedbackStroke = new BasicStroke(
          1 / planScale, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL, 0, 
          new float [] {20 / planScale, 5 / planScale, 5 / planScale, 5 / planScale}, 4 / planScale);
      
      paintCamera(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor, 
          planScale, backgroundColor, foregroundColor);
      
      // Paint alignment feedback depending on aligned object class
      if (this.alignedObjectClass != null) {
        if (Wall.class.isAssignableFrom(this.alignedObjectClass)) {
          paintWallAlignmentFeedback(g2D, (Wall)this.alignedObjectFeedback, this.locationFeeback, this.showPointFeedback, 
              selectionColor, locationFeedbackStroke, planScale,
              selectionOutlinePaint, selectionOutlineStroke);
        } else if (Room.class.isAssignableFrom(this.alignedObjectClass)) {
          paintRoomAlignmentFeedback(g2D, (Room)this.alignedObjectFeedback, this.locationFeeback, this.showPointFeedback,
              selectionColor, locationFeedbackStroke, planScale,
              selectionOutlinePaint, selectionOutlineStroke);
        } else if (DimensionLine.class.isAssignableFrom(this.alignedObjectClass)) {
          paintDimensionLineAlignmentFeedback(g2D, (DimensionLine)this.alignedObjectFeedback, this.locationFeeback, this.showPointFeedback,               
              selectionColor, locationFeedbackStroke, planScale,
              selectionOutlinePaint, selectionOutlineStroke);
        }
      }
      if (this.centerAngleFeedback != null) {
       paintAngleFeedback(g2D, this.centerAngleFeedback, this.point1AngleFeedback, this.point2AngleFeedback, 
           planScale, selectionColor); 
      }
      if (this.dimensionLinesFeedback != null) {
        List<Selectable> emptySelection = Collections.emptyList();        
        paintDimensionLines(g2D, this.dimensionLinesFeedback, emptySelection, 
            null, null, null, locationFeedbackStroke, planScale, 
            backgroundColor, selectionColor, paintMode, true);
      }
      
      if (this.draggedItemsFeedback != null) {
        paintDimensionLines(g2D, Home.getDimensionLinesSubList(this.draggedItemsFeedback), this.draggedItemsFeedback, 
            selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, null, 
            locationFeedbackStroke, planScale, backgroundColor, foregroundColor, paintMode, false);
        paintLabels(g2D, Home.getLabelsSubList(this.draggedItemsFeedback), this.draggedItemsFeedback, 
            selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, null,
            planScale, foregroundColor, paintMode);
        paintRoomsOutline(g2D, this.draggedItemsFeedback, selectionOutlinePaint, selectionOutlineStroke, null, 
            planScale, foregroundColor);
        paintWallsOutline(g2D, this.draggedItemsFeedback, selectionOutlinePaint, selectionOutlineStroke, null, 
            planScale, foregroundColor);
        paintFurniture(g2D, Home.getFurnitureSubList(this.draggedItemsFeedback), selectedItems, planScale, null, 
            foregroundColor, furnitureOutlineColor, paintMode, false);
        paintFurnitureOutline(g2D, this.draggedItemsFeedback, selectionOutlinePaint, selectionOutlineStroke, null, 
            planScale, foregroundColor);
      }
      
      paintRectangleFeedback(g2D, selectionColor, planScale);
    }
  }
  
  /**
   * Paints home items at the given scale, and with background and foreground colors.
   * Outline around selected items will be painted only under <code>PAINT</code> mode. 
   */
  protected void paintHomeItems(Graphics g, float planScale, 
                                Color backgroundColor, Color foregroundColor, PaintMode paintMode) throws InterruptedIOException {
    Graphics2D g2D = (Graphics2D)g;
    List<Selectable> selectedItems = this.home.getSelectedItems();
    if (this.sortedLevelFurniture == null) {
      // Sort home furniture in elevation order
      this.sortedLevelFurniture = new ArrayList<HomePieceOfFurniture>();
      for (HomePieceOfFurniture piece : this.home.getFurniture()) {
        if (isViewableAtSelectedLevel(piece)) {
          this.sortedLevelFurniture.add(piece);
        }
      }
      Collections.sort(this.sortedLevelFurniture,
          new Comparator<HomePieceOfFurniture>() {
            public int compare(HomePieceOfFurniture piece1, HomePieceOfFurniture piece2) {
              float elevationDelta = piece1.getGroundElevation() - piece2.getGroundElevation();
              if (elevationDelta < 0) {
                return -1;
              } else if (elevationDelta > 0) {
                return 1;
              } else {
                return 0;
              }
            }
          });
    }    
    
    Color selectionColor = getSelectionColor(); 
    Paint selectionOutlinePaint = new Color(selectionColor.getRed(), selectionColor.getGreen(), 
        selectionColor.getBlue(), 128);
    Stroke selectionOutlineStroke = new BasicStroke(6 / planScale, 
        BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); 
    Stroke dimensionLinesSelectionOutlineStroke = new BasicStroke(4 / planScale, 
        BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); 
    Stroke locationFeedbackStroke = new BasicStroke(
        1 / planScale, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL, 0, 
        new float [] {20 / planScale, 5 / planScale, 5 / planScale, 5 / planScale}, 4 / planScale);

    paintCompass(g2D, selectedItems, planScale, foregroundColor, paintMode);

    checkCurrentThreadIsntInterrupted(paintMode);
    paintRooms(g2D, selectedItems, planScale, foregroundColor, paintMode);

    checkCurrentThreadIsntInterrupted(paintMode);
    paintWalls(g2D, selectedItems, planScale, backgroundColor, foregroundColor, paintMode);
    
    checkCurrentThreadIsntInterrupted(paintMode);
    paintFurniture(g2D, this.sortedLevelFurniture, selectedItems, 
        planScale, backgroundColor, foregroundColor, getFurnitureOutlineColor(), paintMode, true);
    
    checkCurrentThreadIsntInterrupted(paintMode);
    paintDimensionLines(g2D, this.home.getDimensionLines(), selectedItems, 
        selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, selectionColor, 
        locationFeedbackStroke, planScale, backgroundColor, foregroundColor, paintMode, false);
    
    // Paint rooms text, furniture name and labels last to ensure they are not hidden
    checkCurrentThreadIsntInterrupted(paintMode);
    paintRoomsNameAndArea(g2D, selectedItems, planScale, foregroundColor, paintMode);

    checkCurrentThreadIsntInterrupted(paintMode);
    paintFurnitureName(g2D, this.sortedLevelFurniture, selectedItems, planScale, foregroundColor, paintMode);

    checkCurrentThreadIsntInterrupted(paintMode);
    paintLabels(g2D, this.home.getLabels(), selectedItems, selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, 
        selectionColor, planScale, foregroundColor, paintMode);
    
    if (paintMode == PaintMode.PAINT
        && this.selectedItemsOutlinePainted) {
      paintCompassOutline(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor, 
          planScale, foregroundColor);
      paintRoomsOutline(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor, 
          planScale, foregroundColor);
      paintWallsOutline(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor, 
          planScale, foregroundColor);
      paintFurnitureOutline(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor, 
          planScale, foregroundColor);
    }
  }

  /**
   * Returns the color used to draw selection outlines. 
   */
  protected Color getSelectionColor() {
    if (OperatingSystem.isMacOSX()) {
      if (OperatingSystem.isMacOSXLeopardOrSuperior()) {
        Window window = SwingUtilities.getWindowAncestor(this);
        if (window != null && !window.isActive()) {
          Color selectionColor = UIManager.getColor("List.selectionInactiveBackground");
          if (selectionColor != null) {
            return selectionColor.darker();
          }
        }
        return UIManager.getColor("List.selectionBackground");
      } else { 
        return UIManager.getColor("textHighlight");
      }
    } else {
      // On systems different from Mac OS X, take a darker color
      return UIManager.getColor("textHighlight").darker();
    }
  }

  /**
   * Returns the color used to draw furniture outline of 
   * the shape where a user can click to select a piece of furniture.
   */
  protected Color getFurnitureOutlineColor() {
    return new Color((getForeground().getRGB() & 0xFFFFFF) | 0x55000000, true);
  }
  
  /**
   * Paints rooms. 
   */
  private void paintRooms(Graphics2D g2D, List<Selectable> selectedItems, float planScale, 
                          Color foregroundColor, PaintMode paintMode) {
    if (this.sortedLevelRooms == null) {
      // Sort home rooms in floor / floor-ceiling / ceiling order
      this.sortedLevelRooms = new ArrayList<Room>();
      for (Room room : this.home.getRooms()) {
        if (isViewableAtSelectedLevel(room)) {
          this.sortedLevelRooms.add(room);
        }
      }
      Collections.sort(this.sortedLevelRooms,
          new Comparator<Room>() {
            public int compare(Room room1, Room room2) {
              if (room1.isFloorVisible() == room2.isFloorVisible()
                  && room1.isCeilingVisible() == room2.isCeilingVisible()) {
                return 0; // Keep default order if the rooms have the same visibility
              } else if (!room2.isFloorVisible()
                         || room2.isCeilingVisible()) {
                return 1;
              } else {
                return -1;
              }
            }
          });
    }
    
    Color defaultFillPaint = paintMode == PaintMode.PRINT 
        ? Color.WHITE
        : Color.GRAY;
    // Draw rooms area
    g2D.setStroke(new BasicStroke(getStrokeWidth(Room.class, paintMode) / planScale));
    for (Room room : this.sortedLevelRooms) { 
      boolean selectedRoom = selectedItems.contains(room);
      // In clipboard paint mode, paint room only if it is selected
      if (paintMode != PaintMode.CLIPBOARD
          || selectedRoom) {
        g2D.setPaint(defaultFillPaint);
        if (this.preferences.isRoomFloorColoredOrTextured()
            && room.isFloorVisible()) {
          // Use room floor color or texture image
          if (room.getFloorColor() != null) {
            g2D.setPaint(new Color(room.getFloorColor()));
          } else {
            final HomeTexture floorTexture = room.getFloorTexture();
            if (floorTexture != null) {
              if (this.floorTextureImagesCache == null) {
                this.floorTextureImagesCache = new WeakHashMap<Content, BufferedImage>();
              }
              BufferedImage textureImage = this.floorTextureImagesCache.get(floorTexture.getImage());
              if (textureImage == null
                  || textureImage == WAIT_TEXTURE_IMAGE) {
                final boolean waitForTexture = paintMode != PaintMode.PAINT;
                if (isTextureManagerAvailable()) {
                  // Prefer to share textures images with texture manager if it's available
                  TextureManager.getInstance().loadTexture(floorTexture.getImage(), waitForTexture,
                      new TextureManager.TextureObserver() {
                        public void textureUpdated(Texture texture) {
                          floorTextureImagesCache.put(floorTexture.getImage(), 
                              ((ImageComponent2D)texture.getImage(0)).getImage());
                          if (!waitForTexture) {
                            repaint();
                          }
                        }
                      });
                } else {
                  // Use icon manager if texture manager should be ignored
                  Icon textureIcon = IconManager.getInstance().getIcon(floorTexture.getImage(), 
                      waitForTexture ? null : this);
                  if (IconManager.getInstance().isWaitIcon(textureIcon)) {
                    floorTextureImagesCache.put(floorTexture.getImage(), WAIT_TEXTURE_IMAGE);                    
                  } else if (IconManager.getInstance().isErrorIcon(textureIcon)) {
                    floorTextureImagesCache.put(floorTexture.getImage(), ERROR_TEXTURE_IMAGE);                    
                  } else {
                    BufferedImage textureIconImage = new BufferedImage(
                        textureIcon.getIconWidth(), textureIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
                    Graphics2D g2DIcon = (Graphics2D)textureIconImage.getGraphics();
                    textureIcon.paintIcon(this, g2DIcon, 0, 0);
                    g2DIcon.dispose();
                    floorTextureImagesCache.put(floorTexture.getImage(), textureIconImage);
                  } 
                }
                textureImage = this.floorTextureImagesCache.get(floorTexture.getImage());
              }
              
              g2D.setPaint(new TexturePaint(textureImage, 
                  new Rectangle2D.Float(0, 0, floorTexture.getWidth(), floorTexture.getHeight())));
            }
          }          
        }
        
        Composite oldComposite = setTransparency(g2D, 0.75f);
        g2D.fill(getShape(room.getPoints()));
        g2D.setComposite(oldComposite);

        g2D.setPaint(foregroundColor);
        g2D.draw(getShape(room.getPoints()));
      }
    }
  }

  /**
   * Returns <code>true</code> if <code>TextureManager</code> can be used to manage textures.
   * @return
   */
  private static boolean isTextureManagerAvailable() {
    try {
      return !Boolean.getBoolean("com.eteks.sweethome3d.no3D");
    } catch (AccessControlException ex) {
      // If com.eteks.sweethome3d.no3D can't be read, 
      // security manager won't allow to access to Java 3D DLLs required by TextureManager class too 
    }
    return false;
  }
  
  /**
   * Paints rooms name and area. 
   */
  private void paintRoomsNameAndArea(Graphics2D g2D, List<Selectable> selectedItems, float planScale, 
                                     Color foregroundColor, PaintMode paintMode) {
    g2D.setPaint(foregroundColor);
    Font previousFont = g2D.getFont();
    for (Room room : this.sortedLevelRooms) { 
      boolean selectedRoom = selectedItems.contains(room);
      // In clipboard paint mode, paint room only if it is selected
      if (paintMode != PaintMode.CLIPBOARD
          || selectedRoom) {
        float xRoomCenter = room.getXCenter();
        float yRoomCenter = room.getYCenter();
        String name = room.getName();
        if (name != null) {
          name = name.trim();
          if (name.length() > 0) {
            paintText(g2D, room.getClass(), name, room.getNameStyle(), 
                xRoomCenter + room.getNameXOffset(),
                yRoomCenter + room.getNameYOffset(),
                room.getNameAngle(), previousFont);
          }
        }
        if (room.isAreaVisible()) {
          float area = room.getArea();
          if (area > 0.01f) {
            // Draw room area 
            String areaText = this.preferences.getLengthUnit().getAreaFormatWithUnit().format(area);            
            paintText(g2D, room.getClass(), areaText, room.getAreaStyle(), 
                xRoomCenter + room.getAreaXOffset(),
                yRoomCenter + room.getAreaYOffset(),
                room.getAreaAngle(), previousFont);
          }
        }
      }
    }
    g2D.setFont(previousFont);
  }

  /**
   * Paints the given <code>text</code> centered at the point (<code>x</code>,<code>y</code>).
   */
  private void paintText(Graphics2D g2D,
                         Class<? extends Selectable> selectableClass,
                         String text, TextStyle style, 
                         float x, float y, float angle,
                         Font defaultFont) {
    AffineTransform previousTransform = g2D.getTransform();
    if (style == null) {
      style = this.preferences.getDefaultTextStyle(selectableClass);
    }              
    FontMetrics fontMetrics = getFontMetrics(defaultFont, style);
    Rectangle2D textBounds = fontMetrics.getStringBounds(text, g2D);
    // Draw room name
    g2D.setFont(getFont(defaultFont, style));
    g2D.translate(x, y);
    g2D.rotate(angle);
    g2D.drawString(text, -(float)textBounds.getWidth() / 2, 0);
    g2D.setTransform(previousTransform);
  }
  
  /**
   * Paints the outline of rooms among <code>items</code> and indicators if 
   * <code>items</code> contains only one room and indicator paint isn't <code>null</code>. 
   */
  private void paintRoomsOutline(Graphics2D g2D, List<Selectable> items,   
                          Paint selectionOutlinePaint, Stroke selectionOutlineStroke, 
                          Paint indicatorPaint, float planScale, Color foregroundColor) {
    Collection<Room> rooms = Home.getRoomsSubList(items);
    AffineTransform previousTransform = g2D.getTransform();
    float scaleInverse = 1 / planScale;
    // Draw selection border
    for (Room room : rooms) {
      if (isViewableAtSelectedLevel(room)) {
        g2D.setPaint(selectionOutlinePaint);
        g2D.setStroke(selectionOutlineStroke);
        g2D.draw(getShape(room.getPoints()));
  
        if (indicatorPaint != null) {
          g2D.setPaint(indicatorPaint);         
          // Draw points of the room
          for (float [] point : room.getPoints()) {
            g2D.translate(point [0], point [1]);
            g2D.scale(scaleInverse, scaleInverse);
            g2D.setStroke(POINT_STROKE);
            g2D.fill(WALL_POINT);
            g2D.setTransform(previousTransform);
          }
        }
      }
    }
    
    // Draw rooms area
    g2D.setPaint(foregroundColor);
    g2D.setStroke(new BasicStroke(getStrokeWidth(Room.class, PaintMode.PAINT) / planScale));
    for (Room room : rooms) { 
      if (isViewableAtSelectedLevel(room)) {
        g2D.draw(getShape(room.getPoints()));
      }
    }

    // Paint resize indicators of the room if indicator paint exists
    if (items.size() == 1 
        && rooms.size() == 1 
        && indicatorPaint != null) {
      Room selectedRoom = rooms.iterator().next();
      if (isViewableAtSelectedLevel(selectedRoom)) {
        g2D.setPaint(indicatorPaint);         
        paintRoomResizeIndicators(g2D, selectedRoom, indicatorPaint, planScale);
        paintRoomNameOffsetIndicator(g2D, selectedRoom, indicatorPaint, planScale);
        paintRoomAreaOffsetIndicator(g2D, selectedRoom, indicatorPaint, planScale);
      }
    }
  }

  /**
   * Paints resize indicators on <code>room</code>.
   */
  private void paintRoomResizeIndicators(Graphics2D g2D, Room room,
                                         Paint indicatorPaint, 
                                         float planScale) {
    if (this.resizeIndicatorVisible) {
      g2D.setPaint(indicatorPaint);
      g2D.setStroke(INDICATOR_STROKE);
      AffineTransform previousTransform = g2D.getTransform();
      float scaleInverse = 1 / planScale;
      float [][] points = room.getPoints();
      for (int i = 0; i < points.length; i++) {
        // Draw resize indicator at room point
        float [] point = points [i];
        g2D.translate(point[0], point[1]);
        g2D.scale(scaleInverse, scaleInverse);
        float [] previousPoint = i == 0
            ? points [points.length - 1]
            : points [i -1];
        float [] nextPoint = i == points.length - 1
            ? points [0]
            : points [i + 1];
        // Compute the angle of the mean normalized normal at point i
        float distance1 = (float)Point2D.distance(
            previousPoint [0], previousPoint [1], point [0], point [1]);
        float xNormal1 = (point [1] - previousPoint [1]) / distance1;
        float yNormal1 = (previousPoint [0] - point [0]) / distance1;
        float distance2 = (float)Point2D.distance(
            nextPoint [0], nextPoint [1], point [0], point [1]);
        float xNormal2 = (nextPoint [1] - point [1]) / distance2;
        float yNormal2 = (point [0] - nextPoint [0]) / distance2;
        double angle = Math.atan2(yNormal1 + yNormal2, xNormal1 + xNormal2);         
        // Ensure the indicator will be drawn outside of room 
        if (room.containsPoint(point [0] + (float)Math.cos(angle), 
              point [1] + (float)Math.sin(angle), 0.001f)) {
          angle += Math.PI;
        }        
        g2D.rotate(angle);
        g2D.draw(WALL_AND_LINE_RESIZE_INDICATOR);
        g2D.setTransform(previousTransform);
      }
    }
  }
  
  /**
   * Paints name indicator on <code>room</code>.
   */
  private void paintRoomNameOffsetIndicator(Graphics2D g2D, Room room,
                                            Paint indicatorPaint, 
                                            float planScale) {
    if (this.resizeIndicatorVisible
        && room.getName() != null
        && room.getName().trim().length() > 0) {
      float xName = room.getXCenter() + room.getNameXOffset(); 
      float yName = room.getYCenter() + room.getNameYOffset();
      paintTextIndicators(g2D, room.getClass(), room.getNameStyle(), xName, yName, room.getNameAngle(),
          indicatorPaint, planScale);
    }
  }

  /**
   * Paints resize indicator on <code>room</code>.
   */
  private void paintRoomAreaOffsetIndicator(Graphics2D g2D, Room room,
                                            Paint indicatorPaint, 
                                            float planScale) {
    if (this.resizeIndicatorVisible
        && room.isAreaVisible()
        && room.getArea() > 0.01f) {
      float xArea = room.getXCenter() + room.getAreaXOffset(); 
      float yArea = room.getYCenter() + room.getAreaYOffset();
      paintTextIndicators(g2D, room.getClass(), room.getAreaStyle(), xArea, yArea, room.getAreaAngle(), 
          indicatorPaint, planScale);
    }
  }

  /**
   * Paints text location and angle indicators at the given coordinates.
   */
  private void paintTextIndicators(Graphics2D g2D, 
                                   Class<? extends Selectable> selectableClass,
                                   TextStyle style, 
                                   float x, float y, float angle,
                                   Paint indicatorPaint, 
                                   float planScale) {
    g2D.setPaint(indicatorPaint);
    g2D.setStroke(INDICATOR_STROKE);
    AffineTransform previousTransform = g2D.getTransform();
    float scaleInverse = 1 / planScale;
    g2D.translate(x, y);
    g2D.rotate(angle);
    g2D.scale(scaleInverse, scaleInverse);
    if (Label.class.isAssignableFrom(selectableClass)) {
      g2D.draw(LABEL_CENTER_INDICATOR);      
    } else {
      g2D.draw(TEXT_LOCATION_INDICATOR);
    }
    if (style == null) {
      style = this.preferences.getDefaultTextStyle(selectableClass);
    }              
    FontMetrics fontMetrics = getFontMetrics(g2D.getFont(), style);
    g2D.setTransform(previousTransform);
    g2D.translate(x, y);
    g2D.rotate(angle);
    g2D.translate(0, -fontMetrics.getAscent() * (Label.class.isAssignableFrom(selectableClass) ? 1 : 0.85));
    g2D.scale(scaleInverse, scaleInverse);
    g2D.draw(TEXT_ANGLE_INDICATOR);
    g2D.setTransform(previousTransform);
  }

  /**
   * Paints walls. 
   */
  private void paintWalls(Graphics2D g2D, List<Selectable> selectedItems, float planScale, 
                          Color backgroundColor, Color foregroundColor, PaintMode paintMode) {
    Collection<Wall> paintedWalls;
    Map<Collection<Wall>, Area> wallAreas;
    if (paintMode != PaintMode.CLIPBOARD) {
      wallAreas = getWallAreas();
    } else {
      // In clipboard paint mode, paint only selected walls
      paintedWalls = Home.getWallsSubList(selectedItems);
      wallAreas = getWallAreas(getDrawableWallsInSelectedLevel(paintedWalls));
    }
    float wallPaintScale = paintMode == PaintMode.PRINT 
        ? planScale / 72 * 150 // Adjust scale to 150 dpi for print
        : planScale;
    for (Map.Entry<Collection<Wall>, Area> areaEntry : wallAreas.entrySet()) {
      TextureImage wallPattern = areaEntry.getKey().iterator().next().getPattern();
      fillAndDrawWallsArea(g2D, areaEntry.getValue(), planScale, 
          getWallPaint(wallPaintScale, backgroundColor, foregroundColor, 
              wallPattern != null ? wallPattern : this.preferences.getWallPattern()), foregroundColor, paintMode);
    }
  }

  /**
   * Fills and paints the given area.
   */
  private void fillAndDrawWallsArea(Graphics2D g2D, Area area, float planScale, Paint fillPaint, 
                                    Paint drawPaint, PaintMode paintMode) {
    // Fill walls area
    g2D.setPaint(fillPaint);
    g2D.fill(area);
    // Draw walls area
    g2D.setPaint(drawPaint);
    g2D.setStroke(new BasicStroke(getStrokeWidth(Wall.class, paintMode) / planScale));
    g2D.draw(area);
  }
  
  /**
   * Paints the outline of walls among <code>items</code> and a resize indicator if 
   * <code>items</code> contains only one wall and indicator paint isn't <code>null</code>. 
   */
  private void paintWallsOutline(Graphics2D g2D, List<Selectable> items,   
                                 Paint selectionOutlinePaint, Stroke selectionOutlineStroke, 
                                 Paint indicatorPaint, float planScale, Color foregroundColor) {
    float scaleInverse = 1 / planScale;
    Collection<Wall> walls = Home.getWallsSubList(items);
    AffineTransform previousTransform = g2D.getTransform();
    for (Wall wall : walls) {
      if (isViewableAtSelectedLevel(wall)) {
        // Draw selection border
        g2D.setPaint(selectionOutlinePaint);
        g2D.setStroke(selectionOutlineStroke);
        g2D.draw(getShape(wall.getPoints()));
        
        if (indicatorPaint != null) {
          // Draw start point of the wall
          g2D.translate(wall.getXStart(), wall.getYStart());
          g2D.scale(scaleInverse, scaleInverse);
          g2D.setPaint(indicatorPaint);         
          g2D.setStroke(POINT_STROKE);
          g2D.fill(WALL_POINT);
        
          Float arcExtent = wall.getArcExtent();
          double indicatorAngle;
          double distanceAtScale;
          float xArcCircleCenter = 0;
          float yArcCircleCenter = 0;
          double arcCircleRadius = 0;
          double startPointToEndPointDistance = wall.getStartPointToEndPointDistance();
          double wallAngle = Math.atan2(wall.getYEnd() - wall.getYStart(), 
              wall.getXEnd() - wall.getXStart());
          if (arcExtent != null) {
            xArcCircleCenter = wall.getXArcCircleCenter();
            yArcCircleCenter = wall.getYArcCircleCenter();
            arcCircleRadius = Point2D.distance(wall.getXStart(), wall.getYStart(), 
                xArcCircleCenter, yArcCircleCenter);
            distanceAtScale = arcCircleRadius * Math.abs(arcExtent) * planScale;
            indicatorAngle = Math.atan2(yArcCircleCenter - wall.getYStart(), 
                    xArcCircleCenter - wall.getXStart())  
                + (arcExtent > 0 ? -Math.PI / 2 : Math.PI /2);
          } else {
            distanceAtScale = startPointToEndPointDistance * planScale;
            indicatorAngle = wallAngle;
          }
          // If the distance between start and end points is < 30
          if (distanceAtScale < 30) { 
            // Draw only one orientation indicator between the two points
            g2D.rotate(wallAngle);
            if (arcExtent != null) {
              double wallToStartPointArcCircleCenterAngle = Math.abs(arcExtent) > Math.PI 
                  ? -(Math.PI + arcExtent) / 2
                  : (Math.PI - arcExtent) / 2;
              float arcCircleCenterToWallDistance = (float)(Math.tan(wallToStartPointArcCircleCenterAngle) 
                  * startPointToEndPointDistance / 2); 
              g2D.translate(startPointToEndPointDistance * planScale / 2, 
                  (arcCircleCenterToWallDistance - arcCircleRadius * (Math.abs(wallAngle) > Math.PI / 2 ? -1: 1)) * planScale);
            } else {
              g2D.translate(distanceAtScale / 2, 0);
            }
          } else {
            // Draw orientation indicator at start of the wall
            g2D.rotate(indicatorAngle);
            g2D.translate(8, 0);
          }
          g2D.draw(WALL_ORIENTATION_INDICATOR);
          g2D.setTransform(previousTransform);
          
          // Draw end point of the wall
          g2D.translate(wall.getXEnd(), wall.getYEnd());
          g2D.scale(scaleInverse, scaleInverse);
          g2D.fill(WALL_POINT);
          if (distanceAtScale >= 30) { 
            if (arcExtent != null) {
              indicatorAngle += arcExtent;
            } 
            // Draw orientation indicator at end of the wall
            g2D.rotate(indicatorAngle);
            g2D.translate(-10, 0);
            g2D.draw(WALL_ORIENTATION_INDICATOR);
          }        
          g2D.setTransform(previousTransform);
        }      
      }
    }
    // Draw walls area
    g2D.setPaint(foregroundColor);
    g2D.setStroke(new BasicStroke(getStrokeWidth(Wall.class, PaintMode.PAINT) / planScale));
    for (Area area : getWallAreas(walls).values()) {
      g2D.draw(area);
    }
    
    // Paint resize indicator of the wall if indicator paint exists
    if (items.size() == 1 
        && walls.size() == 1 
        && indicatorPaint != null) {
      Wall wall = walls.iterator().next();
      if (isViewableAtSelectedLevel(wall)) {
        paintWallResizeIndicator(g2D, wall, indicatorPaint, planScale);
      }
    }
  }

  /**
   * Returns <code>true</code> if the given item can be viewed in the plan at the selected level. 
   */
  private boolean isViewableAtSelectedLevel(Elevatable item) {
    Level selectedLevel = this.home.getSelectedLevel();
    return item.getLevel() == null
        || item.getLevel() == selectedLevel
        || item.isAtLevel(selectedLevel);
  }

  /**
   * Paints resize indicator on <code>wall</code>.
   */
  private void paintWallResizeIndicator(Graphics2D g2D, Wall wall,
                                        Paint indicatorPaint, 
                                        float planScale) {
    if (this.resizeIndicatorVisible) {
      g2D.setPaint(indicatorPaint);
      g2D.setStroke(INDICATOR_STROKE);
      
      Float arcExtent = wall.getArcExtent();
      double indicatorAngle;
      if (arcExtent != null) {
        indicatorAngle = Math.atan2(wall.getYArcCircleCenter() - wall.getYEnd(), 
                wall.getXArcCircleCenter() - wall.getXEnd())  
            + (arcExtent > 0 ? -Math.PI / 2 : Math.PI /2);
      } else {
        indicatorAngle = Math.atan2(wall.getYEnd() - wall.getYStart(), 
            wall.getXEnd() - wall.getXStart());
      }
      
      AffineTransform previousTransform = g2D.getTransform();
      float scaleInverse = 1 / planScale;
      // Draw resize indicator at wall end point
      g2D.translate(wall.getXEnd(), wall.getYEnd());
      g2D.scale(scaleInverse, scaleInverse);
      g2D.rotate(indicatorAngle);
      g2D.draw(WALL_AND_LINE_RESIZE_INDICATOR);
      g2D.setTransform(previousTransform);

      if (arcExtent != null) {
        indicatorAngle += Math.PI - arcExtent;
      } else {
        indicatorAngle += Math.PI;
      }
      
      // Draw resize indicator at wall start point
      g2D.translate(wall.getXStart(), wall.getYStart());
      g2D.scale(scaleInverse, scaleInverse);
      g2D.rotate(indicatorAngle);
      g2D.draw(WALL_AND_LINE_RESIZE_INDICATOR);
      g2D.setTransform(previousTransform);
    }
  }
  
  /**
   * Returns areas matching the union of home wall shapes sorted by pattern. 
   */
  private Map<Collection<Wall>, Area> getWallAreas() {
    if (this.wallAreasCache == null) {
      this.wallAreasCache = getWallAreas(getDrawableWallsInSelectedLevel(this.home.getWalls()));
    }
    return this.wallAreasCache;
  }

  /**
   * Returns the walls that belong to the selected level in home.
   */
  private Collection<Wall> getDrawableWallsInSelectedLevel(Collection<Wall> walls) {
    List<Wall> wallsInSelectedLevel = new ArrayList<Wall>();
    for (Wall wall : walls) {
      if (isViewableAtSelectedLevel(wall)) {
        wallsInSelectedLevel.add(wall);
      }
    }
    return wallsInSelectedLevel;
  }
  
  /**
   * Returns areas matching the union of <code>walls</code> shapes sorted by pattern. 
   */
  private Map<Collection<Wall>, Area> getWallAreas(Collection<Wall> walls) {
    if (walls.size() == 0) {
      return Collections.emptyMap();
    }
    // Check if all walls use the same pattern
    TextureImage pattern = walls.iterator().next().getPattern();
    boolean samePattern = true;
    for (Wall wall : walls) {
      if (pattern != wall.getPattern()) {
        samePattern = false;
        break;
      }
    }
    Map<Collection<Wall>, Area> wallAreas = new LinkedHashMap<Collection<Wall>, Area>();
    if (samePattern) {
      wallAreas.put(walls, getItemsArea(walls));
    } else {
      // Create walls sublists by pattern
      Map<TextureImage, Collection<Wall>> sortedWalls = new LinkedHashMap<TextureImage, Collection<Wall>>();
      for (Wall wall : walls) {
        TextureImage wallPattern = wall.getPattern();
        if (wallPattern == null) {
          wallPattern = this.preferences.getWallPattern();
        }
        Collection<Wall> patternWalls = sortedWalls.get(wallPattern);
        if (patternWalls == null) {
          patternWalls = new ArrayList<Wall>();
          sortedWalls.put(wallPattern, patternWalls);
        }
        patternWalls.add(wall);
      }
      for (Collection<Wall> patternWalls : sortedWalls.values()) {
        wallAreas.put(patternWalls, getItemsArea(patternWalls));
      }
    }
    return wallAreas;
  }
  
  /**
   * Returns an area matching the union of all <code>items</code> shapes. 
   */
  private Area getItemsArea(Collection<? extends Selectable> items) {
    Area itemsArea = new Area();
    for (Selectable item : items) {
      itemsArea.add(new Area(getShape(item.getPoints())));
    }
    return itemsArea;
  }

  /**
   * Returns the <code>Paint</code> object used to fill walls.
   */
  private Paint getWallPaint(float planScale, Color backgroundColor, Color foregroundColor, TextureImage wallPattern) {
    BufferedImage patternImage = this.patternImagesCache.get(wallPattern);
    if (patternImage == null
        || !backgroundColor.equals(this.wallsPatternBackgroundCache)
        || !foregroundColor.equals(this.wallsPatternForegroundCache)) {
      patternImage = SwingTools.getPatternImage(wallPattern, backgroundColor, foregroundColor);
      this.patternImagesCache.put(wallPattern, patternImage);
      this.wallsPatternBackgroundCache = backgroundColor;
      this.wallsPatternForegroundCache = foregroundColor;
    }
    return new TexturePaint(patternImage, 
        new Rectangle2D.Float(0, 0, 10 / planScale, 10 / planScale));
  }
  
  /**
   * Paints home furniture.
   */
  private void paintFurniture(Graphics2D g2D, List<HomePieceOfFurniture> furniture, 
                              List<? extends Selectable> selectedItems, float planScale, 
                              Color backgroundColor, Color foregroundColor, 
                              Color furnitureOutlineColor,
                              PaintMode paintMode, boolean paintIcon) {    
    if (!furniture.isEmpty()) {
      BasicStroke pieceBorderStroke = new BasicStroke(getStrokeWidth(HomePieceOfFurniture.class, paintMode) / planScale);
      Boolean allFurnitureViewedFromTop = null;
      // Draw furniture
      for (HomePieceOfFurniture piece : furniture) {
        if (piece.isVisible()) {
          boolean selectedPiece = selectedItems.contains(piece);
          if (piece instanceof HomeFurnitureGroup) {
            List<HomePieceOfFurniture> groupFurniture = ((HomeFurnitureGroup)piece).getFurniture();
            List<Selectable> emptyList = Collections.emptyList();
            paintFurniture(g2D, groupFurniture,  
                selectedPiece 
                    ? groupFurniture 
                    : emptyList, 
                planScale, backgroundColor, foregroundColor, 
                furnitureOutlineColor, paintMode, paintIcon);
          } else if (paintMode != PaintMode.CLIPBOARD
                    || selectedPiece) {
            // In clipboard paint mode, paint piece only if it is selected
            Shape pieceShape = getShape(piece.getPoints());
            Shape pieceShape2D;
            if (piece instanceof HomeDoorOrWindow) {
              HomeDoorOrWindow doorOrWindow = (HomeDoorOrWindow)piece;
              pieceShape2D = getDoorOrWindowShapeAtWallIntersection(doorOrWindow);
              paintDoorOrWindowSashes(g2D, doorOrWindow, planScale, foregroundColor, paintMode);
            } else {
              pieceShape2D = pieceShape;
            }

            boolean viewedFromTop;
            if (this.preferences.isFurnitureViewedFromTop()) {
              if (piece.getPlanIcon() != null
                  || piece instanceof HomeDoorOrWindow) {
                viewedFromTop = true;
              } else {
                if (allFurnitureViewedFromTop == null) {
                  try {
                    // Evaluate allFurnitureViewedFromTop value as late as possible to avoid mandatory dependency towards Java 3D 
                    allFurnitureViewedFromTop = !Boolean.getBoolean("com.eteks.sweethome3d.no3D") 
                        && Component3DManager.getInstance().isOffScreenImageSupported();
                  } catch (AccessControlException ex) {
                    // If com.eteks.sweethome3d.no3D property can't be read, 
                    // security manager won't allow to access to Java 3D DLLs required by PieceOfFurnitureModelIcon class too
                    allFurnitureViewedFromTop = false;
                  }
                }
                viewedFromTop = allFurnitureViewedFromTop.booleanValue();
              }                
            } else {
              viewedFromTop = false;
            }
            if (paintIcon 
                && viewedFromTop) {
              if (piece instanceof HomeDoorOrWindow) {
                // Draw doors and windows border
                g2D.setPaint(backgroundColor);
                g2D.fill(pieceShape2D);
                g2D.setPaint(foregroundColor);
                g2D.setStroke(pieceBorderStroke);
                g2D.draw(pieceShape2D);
              } else {
                paintPieceOfFurnitureTop(g2D, piece, pieceShape2D, pieceBorderStroke, planScale, 
                    backgroundColor, foregroundColor, paintMode);
              }
              if (paintMode == PaintMode.PAINT) {
                // Draw selection outline rectangle  
                g2D.setStroke(pieceBorderStroke);
                g2D.setPaint(furnitureOutlineColor);
                g2D.draw(pieceShape);
              } 
            } else {
              if (paintIcon) {
                // Draw its icon
                paintPieceOfFurnitureIcon(g2D, piece, pieceShape2D, planScale, 
                    backgroundColor, paintMode);
              }
              // Draw its border
              g2D.setPaint(foregroundColor);
              g2D.setStroke(pieceBorderStroke);
              g2D.draw(pieceShape2D);
              if (piece instanceof HomeDoorOrWindow
                  && paintMode == PaintMode.PAINT) {
                // Draw outline rectangle
                g2D.setPaint(furnitureOutlineColor);
                g2D.draw(pieceShape);
              } 
            }
          }
        }
      }
    }
  }

  /**
   * Returns the shape of a door or a window at wall intersection.
   */
  private Shape getDoorOrWindowShapeAtWallIntersection(HomeDoorOrWindow doorOrWindow) {
    // For doors and windows, compute rectangle at wall intersection 
    float wallThickness = doorOrWindow.getDepth() * doorOrWindow.getWallThickness(); 
    float wallDistance  = doorOrWindow.getDepth() * doorOrWindow.getWallDistance(); 
    Rectangle2D doorOrWindowRectangle = new Rectangle2D.Float(
        doorOrWindow.getX() - doorOrWindow.getWidth() / 2,
        doorOrWindow.getY() - doorOrWindow.getDepth() / 2 + wallDistance,
        doorOrWindow.getWidth(), wallThickness);
    // Apply rotation to the rectangle
    AffineTransform rotation = AffineTransform.getRotateInstance(
        doorOrWindow.getAngle(), doorOrWindow.getX(), doorOrWindow.getY());
    PathIterator it = doorOrWindowRectangle.getPathIterator(rotation);
    GeneralPath doorOrWindowShape = new GeneralPath();
    doorOrWindowShape.append(it, false);
    return doorOrWindowShape;
  }

  /**
   * Paints the sashes of a door or a window.
   */
  private void paintDoorOrWindowSashes(Graphics2D g2D, HomeDoorOrWindow doorOrWindow, float planScale, 
                                       Color foregroundColor, PaintMode paintMode) {
    BasicStroke sashBorderStroke = new BasicStroke(getStrokeWidth(HomePieceOfFurniture.class, paintMode) / planScale);
    g2D.setPaint(foregroundColor);
    g2D.setStroke(sashBorderStroke);
    for (Sash sash : doorOrWindow.getSashes()) {
      g2D.draw(getDoorOrWindowSashShape(doorOrWindow, sash));
    }  
  }

  /**
   * Returns the shape of a sash of a door or a window. 
   */
  private GeneralPath getDoorOrWindowSashShape(HomeDoorOrWindow doorOrWindow, 
                                               Sash sash) {
    float modelMirroredSign = doorOrWindow.isModelMirrored() ? -1 : 1;
    float xAxis = modelMirroredSign * sash.getXAxis() * doorOrWindow.getWidth();
    float yAxis = sash.getYAxis() * doorOrWindow.getDepth();
    float sashWidth = sash.getWidth() * doorOrWindow.getWidth();
    float startAngle = (float)Math.toDegrees(sash.getStartAngle());
    if (doorOrWindow.isModelMirrored()) {
      startAngle = 180 - startAngle;
    }
    float extentAngle = modelMirroredSign * (float)Math.toDegrees(sash.getEndAngle() - sash.getStartAngle());
    
    Arc2D arc = new Arc2D.Float(xAxis - sashWidth, yAxis - sashWidth, 
        2 * sashWidth, 2 * sashWidth, 
        startAngle, extentAngle, Arc2D.PIE);
    AffineTransform transformation = AffineTransform.getTranslateInstance(doorOrWindow.getX(), doorOrWindow.getY());
    transformation.rotate(doorOrWindow.getAngle());
    transformation.translate(modelMirroredSign * -doorOrWindow.getWidth() / 2, -doorOrWindow.getDepth() / 2);
    PathIterator it = arc.getPathIterator(transformation);
    GeneralPath sashShape = new GeneralPath();
    sashShape.append(it, false);
    return sashShape;
  }

  /**
   * Paints home furniture visible name.
   */
  private void paintFurnitureName(Graphics2D g2D, List<HomePieceOfFurniture> furniture,
                                  List<? extends Selectable> selectedItems, float planScale, 
                                  Color foregroundColor, PaintMode paintMode) {
    Font previousFont = g2D.getFont();
    g2D.setPaint(foregroundColor);
    // Draw furniture name
    for (HomePieceOfFurniture piece : furniture) {
      if (piece.isVisible()) {
        boolean selectedPiece = selectedItems.contains(piece);
        if (piece instanceof HomeFurnitureGroup) {
          List<HomePieceOfFurniture> groupFurniture = ((HomeFurnitureGroup)piece).getFurniture();
          List<Selectable> emptyList = Collections.emptyList();
          paintFurnitureName(g2D, groupFurniture, 
               selectedPiece 
                   ? groupFurniture 
                   : emptyList, 
               planScale, foregroundColor, paintMode);
        } 
        if (piece.isNameVisible()
            && (paintMode != PaintMode.CLIPBOARD
                || selectedPiece)) {
          // In clipboard paint mode, paint piece only if it is selected
          String name = piece.getName().trim();
          if (name.length() > 0) {
            // Draw piece name
            paintText(g2D, piece.getClass(), name, piece.getNameStyle(), 
                piece.getX() + piece.getNameXOffset(),
                piece.getY() + piece.getNameYOffset(),
                piece.getNameAngle(), previousFont);
          }
        }
      }
    }
    g2D.setFont(previousFont);
  }

  /**
   * Paints the outline of furniture among <code>items</code> and indicators if 
   * <code>items</code> contains only one piece and indicator paint isn't <code>null</code>. 
   */
  private void paintFurnitureOutline(Graphics2D g2D, List<Selectable> items,  
                                     Paint selectionOutlinePaint, Stroke selectionOutlineStroke, 
                                     Paint indicatorPaint, float planScale, 
                                     Color foregroundColor) {
    BasicStroke pieceBorderStroke = new BasicStroke(getStrokeWidth(HomePieceOfFurniture.class, PaintMode.PAINT) / planScale);
    BasicStroke pieceFrontBorderStroke = new BasicStroke(4 * getStrokeWidth(HomePieceOfFurniture.class, PaintMode.PAINT) / planScale, 
        BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
    for (HomePieceOfFurniture piece : Home.getFurnitureSubList(items)) {
      if (piece.isVisible() 
          && isViewableAtSelectedLevel(piece)) {
        float [][] points = piece.getPoints();
        Shape pieceShape = getShape(points);
        
        // Draw selection border
        g2D.setPaint(selectionOutlinePaint);
        g2D.setStroke(selectionOutlineStroke);
        g2D.draw(pieceShape);

        // Draw its border
        g2D.setPaint(foregroundColor);
        g2D.setStroke(pieceBorderStroke);
        g2D.draw(pieceShape);
        
        // Draw its front face with a thicker line
        g2D.setStroke(pieceFrontBorderStroke);
        g2D.draw(new Line2D.Float(points [2][0], points [2][1], points [3][0], points [3][1]));
        
        if (items.size() == 1 && indicatorPaint != null) {
          paintPieceOFFurnitureIndicators(g2D, piece, indicatorPaint, planScale);
        }
      }
    }
  }

  /**
   * Paints <code>piece</code> icon with <code>g2D</code>.
   */
  private void paintPieceOfFurnitureIcon(Graphics2D g2D, HomePieceOfFurniture piece, 
                                         Shape pieceShape2D, float planScale, 
                                         Color backgroundColor, PaintMode paintMode) {
    // Get piece icon
    Icon icon = IconManager.getInstance().getIcon(piece.getIcon(), 128, 
        paintMode == PaintMode.PAINT ? this : null);
    paintPieceOfFurnitureIcon(g2D, piece, icon, pieceShape2D, planScale, backgroundColor);
  }

  /**
   * Paints <code>icon</code> with <code>g2D</code>.
   */
  private void paintPieceOfFurnitureIcon(Graphics2D g2D, HomePieceOfFurniture piece, Icon icon,
                                         Shape pieceShape2D, float planScale, Color backgroundColor) {
    // Fill piece area
    g2D.setPaint(backgroundColor);
    g2D.fill(pieceShape2D);
    
    Shape previousClip = g2D.getClip();
    // Clip icon drawing into piece shape
    g2D.clip(pieceShape2D);
    AffineTransform previousTransform = g2D.getTransform();
    // Translate to piece center
    final Rectangle bounds = pieceShape2D.getBounds();
    g2D.translate(bounds.getCenterX(), bounds.getCenterY());
    float pieceDepth = piece.getDepth();
    if (piece instanceof HomeDoorOrWindow) {
      pieceDepth *= ((HomeDoorOrWindow)piece).getWallThickness(); 
    } 
    // Scale icon to fit in its area
    float minDimension = Math.min(piece.getWidth(), pieceDepth);
    float iconScale = Math.min(1 / planScale, minDimension / icon.getIconHeight());
    // If piece model is mirrored, inverse x scale
    if (piece.isModelMirrored()) {
      g2D.scale(-iconScale, iconScale);
    } else {
      g2D.scale(iconScale, iconScale);
    }
    // Paint piece icon
    icon.paintIcon(this, g2D, -icon.getIconWidth() / 2, -icon.getIconHeight() / 2);
    // Revert g2D transformation to previous value
    g2D.setTransform(previousTransform);
    g2D.setClip(previousClip);
  }

  /**
   * Paints <code>piece</code> top icon with <code>g2D</code>.
   */
  private void paintPieceOfFurnitureTop(Graphics2D g2D, HomePieceOfFurniture piece, 
                                        Shape pieceShape2D, BasicStroke pieceBorderStroke, 
                                        float planScale, 
                                        Color backgroundColor, Color foregroundColor, 
                                        PaintMode paintMode) {
    if (this.furnitureTopViewIconsCache == null) {
      this.furnitureTopViewIconsCache = new WeakHashMap<HomePieceOfFurniture, PieceOfFurnitureTopViewIcon>();
    }
    PieceOfFurnitureTopViewIcon icon = this.furnitureTopViewIconsCache.get(piece);
    if (icon == null
        || icon.isWaitIcon()
           && paintMode != PaintMode.PAINT) {
      PlanComponent waitingComponent = paintMode == PaintMode.PAINT ? this : null;
      // Prefer use plan icon if it exists
      if (piece.getPlanIcon() != null) {
        icon = new PieceOfFurniturePlanIcon(piece, waitingComponent);
      } else {
        icon = new PieceOfFurnitureModelIcon(piece, waitingComponent);
      }
      this.furnitureTopViewIconsCache.put(piece, icon);
    }
    
    if (icon.isWaitIcon() || icon.isErrorIcon()) {
      paintPieceOfFurnitureIcon(g2D, piece, icon, pieceShape2D, planScale, backgroundColor);
      g2D.setPaint(foregroundColor);
      g2D.setStroke(pieceBorderStroke);
      g2D.draw(pieceShape2D);
    } else {        
      AffineTransform previousTransform = g2D.getTransform();  
      // Translate to piece center
      final Rectangle bounds = pieceShape2D.getBounds();
      g2D.translate(bounds.getCenterX(), bounds.getCenterY());
      g2D.rotate(piece.getAngle());
      float pieceDepth = piece.getDepth();
      // Scale icon to fit in its area
      if (piece.isModelMirrored()) {
        // If piece model is mirrored, inverse x scale
        g2D.scale(-piece.getWidth() / icon.getIconWidth(), pieceDepth / icon.getIconHeight());
      } else {
        g2D.scale(piece.getWidth() / icon.getIconWidth(), pieceDepth / icon.getIconHeight());
      }
      // Paint piece icon
      icon.paintIcon(this, g2D, -icon.getIconWidth() / 2, -icon.getIconHeight() / 2);
      // Revert g2D transformation to previous value
      g2D.setTransform(previousTransform);
    }
  }

  /**
   * Paints rotation, elevation, height and resize indicators on <code>piece</code>.
   */
  private void paintPieceOFFurnitureIndicators(Graphics2D g2D, 
                                               HomePieceOfFurniture piece,
                                               Paint indicatorPaint,
                                               float planScale) {
    if (this.resizeIndicatorVisible) {
      g2D.setPaint(indicatorPaint);
      g2D.setStroke(INDICATOR_STROKE);
      
      AffineTransform previousTransform = g2D.getTransform();
      // Draw rotation indicator at top left point of the piece
      float [][] piecePoints = piece.getPoints();
      float scaleInverse = 1 / planScale;
      float pieceAngle = piece.getAngle();
      g2D.translate(piecePoints [0][0], piecePoints [0][1]);
      g2D.scale(scaleInverse, scaleInverse);
      g2D.rotate(pieceAngle);
      g2D.draw(FURNITURE_ROTATION_INDICATOR);
      g2D.setTransform(previousTransform);

      // Draw elevation indicator at top right point of the piece
      g2D.translate(piecePoints [1][0], piecePoints [1][1]);
      g2D.scale(scaleInverse, scaleInverse);
      g2D.rotate(pieceAngle);
      g2D.draw(FURNITURE_ELEVATION_POINT_INDICATOR);
      // Place elevation indicator farther but don't rotate it
      g2D.translate(6.5f, -6.5f);
      g2D.rotate(-pieceAngle); 
      g2D.draw(FURNITURE_ELEVATION_INDICATOR);
      g2D.setTransform(previousTransform);
      
      if (piece.isResizable()) {
        // Draw height indicator at bottom left point of the piece
        g2D.translate(piecePoints [3][0], piecePoints [3][1]);
        g2D.scale(scaleInverse, scaleInverse);
        g2D.rotate(pieceAngle);
        
        if (piece instanceof HomeLight) {
          g2D.draw(LIGHT_POWER_POINT_INDICATOR);
          // Place power indicator farther but don't rotate it
          g2D.translate(-7.5f, 7.5f);
          g2D.rotate(-pieceAngle);
          g2D.draw(LIGHT_POWER_INDICATOR);
        } else {
          g2D.draw(FURNITURE_HEIGHT_POINT_INDICATOR);
          // Place height indicator farther but don't rotate it
          g2D.translate(-7.5f, 7.5f);
          g2D.rotate(-pieceAngle);
          g2D.draw(FURNITURE_HEIGHT_INDICATOR);
        }
        g2D.setTransform(previousTransform);
        
        // Draw resize indicator at top left point of the piece
        g2D.translate(piecePoints [2][0], piecePoints [2][1]);
        g2D.scale(scaleInverse, scaleInverse);
        g2D.rotate(pieceAngle);
        g2D.draw(FURNITURE_RESIZE_INDICATOR);
        g2D.setTransform(previousTransform);
      }
      
      if (piece.isNameVisible()
          && piece.getName().trim().length() > 0) {
        float xName = piece.getX() + piece.getNameXOffset(); 
        float yName = piece.getY() + piece.getNameYOffset();
        paintTextIndicators(g2D, piece.getClass(), piece.getNameStyle(), xName, yName, piece.getNameAngle(),
            indicatorPaint, planScale);
      }
    }
  }
  
  /**
   * Paints dimension lines. 
   */
  private void paintDimensionLines(Graphics2D g2D, 
                          Collection<DimensionLine> dimensionLines, List<Selectable> selectedItems,   
                          Paint selectionOutlinePaint, Stroke selectionOutlineStroke, 
                          Paint indicatorPaint, Stroke extensionLineStroke, float planScale, 
                          Color backgroundColor, Color foregroundColor, 
                          PaintMode paintMode, boolean feedback) {
    // In clipboard paint mode, paint only selected dimension lines
    if (paintMode == PaintMode.CLIPBOARD) {
      dimensionLines = Home.getDimensionLinesSubList(selectedItems);
    }

    // Draw dimension lines
    g2D.setPaint(foregroundColor);
    BasicStroke dimensionLineStroke = new BasicStroke(getStrokeWidth(DimensionLine.class, paintMode) / planScale);
    // Change font size
    Font previousFont = g2D.getFont();
    for (DimensionLine dimensionLine : dimensionLines) {
      if (isViewableAtSelectedLevel(dimensionLine)) {
        AffineTransform previousTransform = g2D.getTransform();
        double angle = Math.atan2(dimensionLine.getYEnd() - dimensionLine.getYStart(), 
            dimensionLine.getXEnd() - dimensionLine.getXStart());
        float dimensionLineLength = dimensionLine.getLength();
        g2D.translate(dimensionLine.getXStart(), dimensionLine.getYStart());
        g2D.rotate(angle);
        g2D.translate(0, dimensionLine.getOffset());
        
        if (paintMode == PaintMode.PAINT
            && this.selectedItemsOutlinePainted
            && selectedItems.contains(dimensionLine)) {
          // Draw selection border
          g2D.setPaint(selectionOutlinePaint);
          g2D.setStroke(selectionOutlineStroke);
          // Draw dimension line
          g2D.draw(new Line2D.Float(0, 0, dimensionLineLength, 0));
          // Draw dimension line ends
          g2D.draw(DIMENSION_LINE_END);
          g2D.translate(dimensionLineLength, 0);
          g2D.draw(DIMENSION_LINE_END);
          g2D.translate(-dimensionLineLength, 0);
          // Draw extension lines
          g2D.draw(new Line2D.Float(0, -dimensionLine.getOffset(), 0, -5));
          g2D.draw(new Line2D.Float(dimensionLineLength, -dimensionLine.getOffset(), dimensionLineLength, -5));
          
          g2D.setPaint(foregroundColor);
        }
        
        g2D.setStroke(dimensionLineStroke);
        // Draw dimension line
        g2D.draw(new Line2D.Float(0, 0, dimensionLineLength, 0));
        // Draw dimension line ends
        g2D.draw(DIMENSION_LINE_END);
        g2D.translate(dimensionLineLength, 0);
        g2D.draw(DIMENSION_LINE_END);
        g2D.translate(-dimensionLineLength, 0);
        // Draw extension lines
        g2D.setStroke(extensionLineStroke);
        g2D.draw(new Line2D.Float(0, -dimensionLine.getOffset(), 0, -5));
        g2D.draw(new Line2D.Float(dimensionLineLength, -dimensionLine.getOffset(), dimensionLineLength, -5));
        
        float displayedValue = feedback
            ? this.preferences.getLengthUnit().getMagnetizedLength(dimensionLineLength, getPixelLength())
            : dimensionLineLength;
        String lengthText = this.preferences.getLengthUnit().getFormat().format(displayedValue);
        TextStyle lengthStyle = dimensionLine.getLengthStyle();
        if (lengthStyle == null) {
          lengthStyle = this.preferences.getDefaultTextStyle(dimensionLine.getClass());
        }          
        if (feedback) {
          // Use default for feedback
          lengthStyle = lengthStyle.deriveStyle(getFont().getSize() / getScale());
        }
        Font font = getFont(previousFont, lengthStyle);
        FontMetrics lengthFontMetrics = getFontMetrics(font, lengthStyle);
        Rectangle2D lengthTextBounds = lengthFontMetrics.getStringBounds(lengthText, g2D);
        int fontAscent = lengthFontMetrics.getAscent();
        g2D.translate((dimensionLineLength - (float)lengthTextBounds.getWidth()) / 2, 
            dimensionLine.getOffset() <= 0 
                ? -lengthFontMetrics.getDescent() - 1
                : fontAscent + 1);
        if (feedback) {
          // Draw text outline with half transparent background color
          g2D.setPaint(backgroundColor);
          Composite oldComposite = setTransparency(g2D, 0.7f);
          g2D.setStroke(new BasicStroke(3 / planScale));
          FontRenderContext fontRenderContext = g2D.getFontRenderContext();
          TextLayout textLayout = new TextLayout(lengthText, font, fontRenderContext);
          g2D.draw(textLayout.getOutline(new AffineTransform()));
          g2D.setComposite(oldComposite);
          g2D.setPaint(foregroundColor);
        }
        // Draw dimension length in middle
        g2D.setFont(font);
        g2D.drawString(lengthText, 0, 0);
        
        g2D.setTransform(previousTransform);
      }
    }
    g2D.setFont(previousFont);
    // Paint resize indicator of selected dimension line
    if (selectedItems.size() == 1 
        && selectedItems.get(0) instanceof DimensionLine
        && paintMode == PaintMode.PAINT
        && indicatorPaint != null) {
      paintDimensionLineResizeIndicator(g2D, (DimensionLine)selectedItems.get(0), indicatorPaint, planScale);
    }
  }

  /**
   * Paints resize indicator on a given dimension line.
   */
  private void paintDimensionLineResizeIndicator(Graphics2D g2D, DimensionLine dimensionLine,
                                                 Paint indicatorPaint, 
                                                 float planScale) {
    if (this.resizeIndicatorVisible) {
      g2D.setPaint(indicatorPaint);
      g2D.setStroke(INDICATOR_STROKE);

      double wallAngle = Math.atan2(dimensionLine.getYEnd() - dimensionLine.getYStart(), 
          dimensionLine.getXEnd() - dimensionLine.getXStart());
      
      AffineTransform previousTransform = g2D.getTransform();
      float scaleInverse = 1 / planScale;
      // Draw resize indicator at the start of dimension line 
      g2D.translate(dimensionLine.getXStart(), dimensionLine.getYStart());
      g2D.rotate(wallAngle);
      g2D.translate(0, dimensionLine.getOffset());
      g2D.rotate(Math.PI);
      g2D.scale(scaleInverse, scaleInverse);
      g2D.draw(WALL_AND_LINE_RESIZE_INDICATOR);
      g2D.setTransform(previousTransform);
      
      // Draw resize indicator at the end of dimension line 
      g2D.translate(dimensionLine.getXEnd(), dimensionLine.getYEnd());
      g2D.rotate(wallAngle);
      g2D.translate(0, dimensionLine.getOffset());
      g2D.scale(scaleInverse, scaleInverse);
      g2D.draw(WALL_AND_LINE_RESIZE_INDICATOR);
      g2D.setTransform(previousTransform);

      // Draw resize indicator at the middle of dimension line
      g2D.translate((dimensionLine.getXStart() + dimensionLine.getXEnd()) / 2, 
          (dimensionLine.getYStart() + dimensionLine.getYEnd()) / 2);
      g2D.rotate(wallAngle);
      g2D.translate(0, dimensionLine.getOffset());
      g2D.rotate(dimensionLine.getOffset() <= 0 
          ? Math.PI / 2 
          : -Math.PI / 2);
      g2D.scale(scaleInverse, scaleInverse);
      g2D.draw(WALL_AND_LINE_RESIZE_INDICATOR);
      g2D.setTransform(previousTransform);
    }
  }
  
  /**
   * Paints home labels.
   */
  private void paintLabels(Graphics2D g2D, Collection<Label> labels, List<Selectable> selectedItems, 
                           Paint selectionOutlinePaint, Stroke selectionOutlineStroke, Paint indicatorPaint,
                           float planScale, Color foregroundColor, PaintMode paintMode) {
    Font previousFont = g2D.getFont();
    // Draw labels
    for (Label label : labels) {
      if (isViewableAtSelectedLevel(label)) {
        boolean selectedLabel = selectedItems.contains(label);
        // In clipboard paint mode, paint label only if it is selected
        if (paintMode != PaintMode.CLIPBOARD || selectedLabel) {
          String labelText = label.getText();
          float xLabel = label.getX();
          float yLabel = label.getY();
          float labelAngle = label.getAngle();
          TextStyle labelStyle = label.getStyle();
          if (labelStyle == null) {
            labelStyle = this.preferences.getDefaultTextStyle(label.getClass());
          }          
          g2D.setPaint(foregroundColor);
          paintText(g2D, label.getClass(), labelText, labelStyle, 
              xLabel, yLabel, labelAngle, previousFont);

          if (paintMode == PaintMode.PAINT && this.selectedItemsOutlinePainted && selectedLabel) {
            // Draw selection border
            g2D.setPaint(selectionOutlinePaint);
            g2D.setStroke(selectionOutlineStroke);
            g2D.draw(getShape(getTextBounds(labelText, labelStyle, xLabel, yLabel, labelAngle)));
            g2D.setPaint(foregroundColor);
            if (indicatorPaint != null) {
              paintTextIndicators(g2D, label.getClass(), labelStyle, xLabel, yLabel, labelAngle,
                  indicatorPaint, planScale);
            }
          }
        }
      }
    }
    g2D.setFont(previousFont);
  }

  /**
   * Paints the compass. 
   */
  private void paintCompass(Graphics2D g2D, List<Selectable> selectedItems, float planScale, 
                            Color foregroundColor, PaintMode paintMode) {
    Compass compass = this.home.getCompass();
    if (compass.isVisible() 
        && (paintMode != PaintMode.CLIPBOARD
            || selectedItems.contains(compass))) {
      AffineTransform previousTransform = g2D.getTransform();
      g2D.translate(compass.getX(), compass.getY());
      g2D.rotate(compass.getNorthDirection());
      float diameter = compass.getDiameter();
      g2D.scale(diameter, diameter);
      g2D.setColor(foregroundColor);
      g2D.fill(COMPASS);
      g2D.setTransform(previousTransform);
    }
  }

  /**
   * Paints the outline of the compass when it's belongs to <code>items</code>. 
   */
  private void paintCompassOutline(Graphics2D g2D, List<Selectable> items,
                                   Paint selectionOutlinePaint, Stroke selectionOutlineStroke, 
                                   Paint indicatorPaint, float planScale, Color foregroundColor) {
    Compass compass = this.home.getCompass();
    if (items.contains(compass) 
        && compass.isVisible()) {
      AffineTransform previousTransform = g2D.getTransform();
      g2D.translate(compass.getX(), compass.getY());
      g2D.rotate(compass.getNorthDirection());
      float diameter = compass.getDiameter();
      g2D.scale(diameter, diameter);
  
      g2D.setPaint(selectionOutlinePaint);
      g2D.setStroke(new BasicStroke((5.5f + planScale) / diameter / planScale));
      g2D.draw(COMPASS_DISC);
      g2D.setColor(foregroundColor);
      g2D.setStroke(new BasicStroke(1f / diameter / planScale));
      g2D.draw(COMPASS_DISC);
      g2D.setTransform(previousTransform);

      // Paint indicators of the compass 
      if (items.size() == 1 
          && items.get(0) == compass) {
        g2D.setPaint(indicatorPaint);         
        paintCompassIndicators(g2D, compass, indicatorPaint, planScale);
      }
    }
  }

  private void paintCompassIndicators(Graphics2D g2D, 
                                      Compass compass, Paint indicatorPaint,
                                      float planScale) {
    if (this.resizeIndicatorVisible) {
      g2D.setPaint(indicatorPaint);
      g2D.setStroke(INDICATOR_STROKE);
      
      AffineTransform previousTransform = g2D.getTransform();
      // Draw rotation indicator at middle of second and third point of compass 
      float [][] compassPoints = compass.getPoints();
      float scaleInverse = 1 / planScale;
      g2D.translate((compassPoints [2][0] + compassPoints [3][0]) / 2, 
          (compassPoints [2][1] + compassPoints [3][1]) / 2);
      g2D.scale(scaleInverse, scaleInverse);
      g2D.rotate(compass.getNorthDirection());
      g2D.draw(COMPASS_ROTATION_INDICATOR);
      g2D.setTransform(previousTransform);

      // Draw resize indicator at middle of second and third point of compass
      g2D.translate((compassPoints [1][0] + compassPoints [2][0]) / 2, 
          (compassPoints [1][1] + compassPoints [2][1]) / 2);
      g2D.scale(scaleInverse, scaleInverse);
      g2D.rotate(compass.getNorthDirection());
      g2D.draw(COMPASS_RESIZE_INDICATOR);
      g2D.setTransform(previousTransform);
    }
  }

  /**
   * Paints wall location feedback.
   */
  private void paintWallAlignmentFeedback(Graphics2D g2D, 
                                          Wall alignedWall, Point2D locationFeedback, 
                                          boolean showPointFeedback, 
                                          Paint feedbackPaint, Stroke feedbackStroke,
                                          float planScale, Paint pointPaint, 
                                          Stroke pointStroke) {
    // Paint wall location feedback
    if (locationFeedback != null) {
      float margin = 0.5f / planScale;
      // Search which wall start or end point is at locationFeedback abscissa or ordinate
      // ignoring the start and end point of alignedWall
      float x = (float)locationFeedback.getX(); 
      float y = (float)locationFeedback.getY();
      float deltaXToClosestWall = Float.POSITIVE_INFINITY;
      float deltaYToClosestWall = Float.POSITIVE_INFINITY;
      for (Wall wall : getViewedItems(this.home.getWalls(), this.otherLevelWallsCache)) {
        if (wall != alignedWall) {
          if (Math.abs(x - wall.getXStart()) < margin
              && (alignedWall == null
                  || !equalsWallPoint(wall.getXStart(), wall.getYStart(), alignedWall))) {
            if (Math.abs(deltaYToClosestWall) > Math.abs(y - wall.getYStart())) {
              deltaYToClosestWall = y - wall.getYStart();
            }
          } else if (Math.abs(x - wall.getXEnd()) < margin
                    && (alignedWall == null
                        || !equalsWallPoint(wall.getXEnd(), wall.getYEnd(), alignedWall))) {
            if (Math.abs(deltaYToClosestWall) > Math.abs(y - wall.getYEnd())) {
              deltaYToClosestWall = y - wall.getYEnd();
            }                
          }
          
          if (Math.abs(y - wall.getYStart()) < margin
              && (alignedWall == null
                  || !equalsWallPoint(wall.getXStart(), wall.getYStart(), alignedWall))) {
            if (Math.abs(deltaXToClosestWall) > Math.abs(x - wall.getXStart())) {
              deltaXToClosestWall = x - wall.getXStart();
            }
          } else if (Math.abs(y - wall.getYEnd()) < margin
                    && (alignedWall == null
                        || !equalsWallPoint(wall.getXEnd(), wall.getYEnd(), alignedWall))) {
            if (Math.abs(deltaXToClosestWall) > Math.abs(x - wall.getXEnd())) {
              deltaXToClosestWall = x - wall.getXEnd();
            }                
          }

          float [][] wallPoints = wall.getPoints();
          // Take into account only points at start and end of the wall
          wallPoints = new float [][] {wallPoints [0], wallPoints [wallPoints.length / 2 - 1], 
                                       wallPoints [wallPoints.length / 2], wallPoints [wallPoints.length - 1]}; 
          for (int i = 0; i < wallPoints.length; i++) {
            if (Math.abs(x - wallPoints [i][0]) < margin
                && (alignedWall == null
                    || !equalsWallPoint(wallPoints [i][0], wallPoints [i][1], alignedWall))) {
              if (Math.abs(deltaYToClosestWall) > Math.abs(y - wallPoints [i][1])) {
                deltaYToClosestWall = y - wallPoints [i][1];
              }
            }
            if (Math.abs(y - wallPoints [i][1]) < margin
                && (alignedWall == null
                    || !equalsWallPoint(wallPoints [i][0], wallPoints [i][1], alignedWall))) {
              if (Math.abs(deltaXToClosestWall) > Math.abs(x - wallPoints [i][0])) {
                deltaXToClosestWall = x - wallPoints [i][0];
              }                
            }
          }
        }
      }
      
      // Draw alignment horizontal and vertical lines
      g2D.setPaint(feedbackPaint);         
      g2D.setStroke(feedbackStroke);
      if (deltaXToClosestWall != Float.POSITIVE_INFINITY) {
        if (deltaXToClosestWall > 0) {
          g2D.draw(new Line2D.Float(x + 25 / planScale, y, 
              x - deltaXToClosestWall - 25 / planScale, y));
        } else {
          g2D.draw(new Line2D.Float(x - 25 / planScale, y, 
              x - deltaXToClosestWall + 25 / planScale, y));
        }
      }

      if (deltaYToClosestWall != Float.POSITIVE_INFINITY) {
        if (deltaYToClosestWall > 0) {
          g2D.draw(new Line2D.Float(x, y + 25 / planScale, 
              x, y - deltaYToClosestWall - 25 / planScale));
        } else {
          g2D.draw(new Line2D.Float(x, y - 25 / planScale, 
              x, y - deltaYToClosestWall + 25 / planScale));
        }
      }

      // Draw point feedback
      if (showPointFeedback) {
        paintPointFeedback(g2D, locationFeedback, feedbackPaint, planScale, pointPaint, pointStroke);
      }
    }
  }

  /**
   * Returns the items viewed in the plan at the selected level.
   */
  private <T extends Elevatable> Collection<T> getViewedItems(Collection<T> homeItems, List<T> otherLevelItems) {
    List<T> viewedWalls = new ArrayList<T>();
    if (otherLevelItems != null) {
      viewedWalls.addAll(otherLevelItems);
    }
    for (T wall : homeItems) {
      if (isViewableAtSelectedLevel(wall)) {
        viewedWalls.add(wall);
      }
    }
    return viewedWalls;
  }

  /**
   * Paints point feedback.
   */
  private void paintPointFeedback(Graphics2D g2D, Point2D locationFeedback, 
                                  Paint feedbackPaint, float planScale,
                                  Paint pointPaint, Stroke pointStroke) {
    g2D.setPaint(pointPaint);         
    g2D.setStroke(pointStroke);
    g2D.draw(new Ellipse2D.Float((float)locationFeedback.getX() - 5f / planScale, 
        (float)locationFeedback.getY() - 5f / planScale, 10f / planScale, 10f / planScale));
    g2D.setPaint(feedbackPaint);         
    g2D.setStroke(new BasicStroke(1 / planScale));
    g2D.draw(new Line2D.Float((float)locationFeedback.getX(), 
        (float)locationFeedback.getY() - 5f / planScale, 
        (float)locationFeedback.getX(), 
        (float)locationFeedback.getY() + 5f / planScale));
    g2D.draw(new Line2D.Float((float)locationFeedback.getX() - 5f / planScale, 
        (float)locationFeedback.getY(), 
        (float)locationFeedback.getX() + 5f / planScale, 
        (float)locationFeedback.getY()));
  }
  
  /**
   * Returns <code>true</code> if <code>wall</code> start or end point 
   * equals the point (<code>x</code>, <code>y</code>).
   */
  private boolean equalsWallPoint(float x, float y, Wall wall) {
    return x == wall.getXStart() && y == wall.getYStart()
           || x == wall.getXEnd() && y == wall.getYEnd();
  }

  /**
   * Paints room location feedback.
   */
  private void paintRoomAlignmentFeedback(Graphics2D g2D, 
                                          Room alignedRoom, Point2D locationFeedback, 
                                          boolean showPointFeedback, 
                                          Paint feedbackPaint, Stroke feedbackStroke,
                                          float planScale, Paint pointPaint, 
                                          Stroke pointStroke) {
    // Paint room location feedback
    if (locationFeedback != null) {
      float margin = 0.5f / planScale;
      // Search which room points are at locationFeedback abscissa or ordinate
      float x = (float)locationFeedback.getX(); 
      float y = (float)locationFeedback.getY();
      float deltaXToClosestObject = Float.POSITIVE_INFINITY;
      float deltaYToClosestObject = Float.POSITIVE_INFINITY;
      for (Room room : getViewedItems(this.home.getRooms(), this.otherLevelRoomsCache)) {
        float [][] roomPoints = room.getPoints();
        int editedPointIndex = -1;
        if (room == alignedRoom) {
          // Search which room point could match location feedback 
          for (int i = 0; i < roomPoints.length; i++) {
            if (roomPoints [i][0] == x && roomPoints [i][1] == y) {
              editedPointIndex = i;
              break;
            }
          }
        }
        for (int i = 0; i < roomPoints.length; i++) {
          if (editedPointIndex == -1 || (i != editedPointIndex && roomPoints.length > 2)) {
            if (Math.abs(x - roomPoints [i][0]) < margin
                && Math.abs(deltaYToClosestObject) > Math.abs(y - roomPoints [i][1])) {
              deltaYToClosestObject = y - roomPoints [i][1];
            }
            if (Math.abs(y - roomPoints [i][1]) < margin
                && Math.abs(deltaXToClosestObject) > Math.abs(x - roomPoints [i][0])) {
              deltaXToClosestObject = x - roomPoints [i][0];
            }
          }
        }
      }
      // Search which wall points are at locationFeedback abscissa or ordinate
      for (Wall wall : getViewedItems(this.home.getWalls(), this.otherLevelWallsCache)) {
        float [][] wallPoints = wall.getPoints();
        // Take into account only points at start and end of the wall
        wallPoints = new float [][] {wallPoints [0], wallPoints [wallPoints.length / 2 - 1], 
                                     wallPoints [wallPoints.length / 2], wallPoints [wallPoints.length - 1]}; 
        for (int i = 0; i < wallPoints.length; i++) {
          if (Math.abs(x - wallPoints [i][0]) < margin
              && Math.abs(deltaYToClosestObject) > Math.abs(y - wallPoints [i][1])) {
            deltaYToClosestObject = y - wallPoints [i][1];
          }
          if (Math.abs(y - wallPoints [i][1]) < margin
              && Math.abs(deltaXToClosestObject) > Math.abs(x - wallPoints [i][0])) {
            deltaXToClosestObject = x - wallPoints [i][0];
          }
        }
      }
      
      // Draw alignment horizontal and vertical lines
      g2D.setPaint(feedbackPaint);         
      g2D.setStroke(feedbackStroke);
      if (deltaXToClosestObject != Float.POSITIVE_INFINITY) {
        if (deltaXToClosestObject > 0) {
          g2D.draw(new Line2D.Float(x + 25 / planScale, y, 
              x - deltaXToClosestObject - 25 / planScale, y));
        } else {
          g2D.draw(new Line2D.Float(x - 25 / planScale, y, 
              x - deltaXToClosestObject + 25 / planScale, y));
        }
      }

      if (deltaYToClosestObject != Float.POSITIVE_INFINITY) {
        if (deltaYToClosestObject > 0) {
          g2D.draw(new Line2D.Float(x, y + 25 / planScale, 
              x, y - deltaYToClosestObject - 25 / planScale));
        } else {
          g2D.draw(new Line2D.Float(x, y - 25 / planScale, 
              x, y - deltaYToClosestObject + 25 / planScale));
        }
      }
      
      if (showPointFeedback) {
        paintPointFeedback(g2D, locationFeedback, feedbackPaint, planScale, pointPaint, pointStroke);
      }
    }
  }

  /**
   * Paints dimension line location feedback.
   */
  private void paintDimensionLineAlignmentFeedback(Graphics2D g2D, 
                                                   DimensionLine alignedDimensionLine, Point2D locationFeedback, 
                                                   boolean showPointFeedback, 
                                                   Paint feedbackPaint, Stroke feedbackStroke,
                                                   float planScale, Paint pointPaint, 
                                                   Stroke pointStroke) {
    // Paint dimension line location feedback
    if (locationFeedback != null) {      
      float margin = 0.5f / planScale;
      // Search which room points are at locationFeedback abscissa or ordinate
      float x = (float)locationFeedback.getX(); 
      float y = (float)locationFeedback.getY();
      float deltaXToClosestObject = Float.POSITIVE_INFINITY;
      float deltaYToClosestObject = Float.POSITIVE_INFINITY;
      for (Room room : getViewedItems(this.home.getRooms(), this.otherLevelRoomsCache)) {
        float [][] roomPoints = room.getPoints();
        for (int i = 0; i < roomPoints.length; i++) {
          if (Math.abs(x - roomPoints [i][0]) < margin
              && Math.abs(deltaYToClosestObject) > Math.abs(y - roomPoints [i][1])) {
            deltaYToClosestObject = y - roomPoints [i][1];
          }
          if (Math.abs(y - roomPoints [i][1]) < margin
              && Math.abs(deltaXToClosestObject) > Math.abs(x - roomPoints [i][0])) {
            deltaXToClosestObject = x - roomPoints [i][0];
          }
        }
      }
      // Search which dimension line start or end point is at locationFeedback abscissa or ordinate
      // ignoring the start and end point of alignedDimensionLine
      for (DimensionLine dimensionLine : this.home.getDimensionLines()) {
        if (isViewableAtSelectedLevel(dimensionLine) 
            && dimensionLine != alignedDimensionLine) {
          if (Math.abs(x - dimensionLine.getXStart()) < margin
              && (alignedDimensionLine == null
                  || !equalsDimensionLinePoint(dimensionLine.getXStart(), dimensionLine.getYStart(), 
                          alignedDimensionLine))) {
            if (Math.abs(deltaYToClosestObject) > Math.abs(y - dimensionLine.getYStart())) {
              deltaYToClosestObject = y - dimensionLine.getYStart();
            }
          } else if (Math.abs(x - dimensionLine.getXEnd()) < margin
                    && (alignedDimensionLine == null
                        || !equalsDimensionLinePoint(dimensionLine.getXEnd(), dimensionLine.getYEnd(), 
                                alignedDimensionLine))) {
            if (Math.abs(deltaYToClosestObject) > Math.abs(y - dimensionLine.getYEnd())) {
              deltaYToClosestObject = y - dimensionLine.getYEnd();
            }                
          }
          if (Math.abs(y - dimensionLine.getYStart()) < margin
              && (alignedDimensionLine == null
                  || !equalsDimensionLinePoint(dimensionLine.getXStart(), dimensionLine.getYStart(), 
                          alignedDimensionLine))) {
            if (Math.abs(deltaXToClosestObject) > Math.abs(x - dimensionLine.getXStart())) {
              deltaXToClosestObject = x - dimensionLine.getXStart();
            }
          } else if (Math.abs(y - dimensionLine.getYEnd()) < margin
                    && (alignedDimensionLine == null
                        || !equalsDimensionLinePoint(dimensionLine.getXEnd(), dimensionLine.getYEnd(), 
                                alignedDimensionLine))) {
            if (Math.abs(deltaXToClosestObject) > Math.abs(x - dimensionLine.getXEnd())) {
              deltaXToClosestObject = x - dimensionLine.getXEnd();
            }                
          }
        }
      }
      // Search which wall points are at locationFeedback abscissa or ordinate
      for (Wall wall : getViewedItems(this.home.getWalls(), this.otherLevelWallsCache)) {
        float [][] wallPoints = wall.getPoints();
        // Take into account only points at start and end of the wall
        wallPoints = new float [][] {wallPoints [0], wallPoints [wallPoints.length / 2 - 1], 
                                     wallPoints [wallPoints.length / 2], wallPoints [wallPoints.length - 1]}; 
        for (int i = 0; i < wallPoints.length; i++) {
          if (Math.abs(x - wallPoints [i][0]) < margin
              && Math.abs(deltaYToClosestObject) > Math.abs(y - wallPoints [i][1])) {
            deltaYToClosestObject = y - wallPoints [i][1];
          }
          if (Math.abs(y - wallPoints [i][1]) < margin
              && Math.abs(deltaXToClosestObject) > Math.abs(x - wallPoints [i][0])) {
            deltaXToClosestObject = x - wallPoints [i][0];
          }
        }
      }
      // Search which piece of furniture points are at locationFeedback abscissa or ordinate
      for (HomePieceOfFurniture piece : this.home.getFurniture()) {
        if (piece.isVisible()
            && isViewableAtSelectedLevel(piece)) {
          float [][] piecePoints = piece.getPoints();
          for (int i = 0; i < piecePoints.length; i++) {
            if (Math.abs(x - piecePoints [i][0]) < margin
                && Math.abs(deltaYToClosestObject) > Math.abs(y - piecePoints [i][1])) {
              deltaYToClosestObject = y - piecePoints [i][1];
            }
            if (Math.abs(y - piecePoints [i][1]) < margin
                && Math.abs(deltaXToClosestObject) > Math.abs(x - piecePoints [i][0])) {
              deltaXToClosestObject = x - piecePoints [i][0];
            }

          }
        }
      }
      
      // Draw alignment horizontal and vertical lines
      g2D.setPaint(feedbackPaint);         
      g2D.setStroke(feedbackStroke);
      if (deltaXToClosestObject != Float.POSITIVE_INFINITY) {
        if (deltaXToClosestObject > 0) {
          g2D.draw(new Line2D.Float(x + 25 / planScale, y, 
              x - deltaXToClosestObject - 25 / planScale, y));
        } else {
          g2D.draw(new Line2D.Float(x - 25 / planScale, y, 
              x - deltaXToClosestObject + 25 / planScale, y));
        }
      }

      if (deltaYToClosestObject != Float.POSITIVE_INFINITY) {
        if (deltaYToClosestObject > 0) {
          g2D.draw(new Line2D.Float(x, y + 25 / planScale, 
              x, y - deltaYToClosestObject - 25 / planScale));
        } else {
          g2D.draw(new Line2D.Float(x, y - 25 / planScale, 
              x, y - deltaYToClosestObject + 25 / planScale));
        }
      }

      if (showPointFeedback) {
        paintPointFeedback(g2D, locationFeedback, feedbackPaint, planScale, pointPaint, pointStroke);
      }
    }
  }
  
  /**
   * Returns <code>true</code> if <code>dimensionLine</code> start or end point 
   * equals the point (<code>x</code>, <code>y</code>).
   */
  private boolean equalsDimensionLinePoint(float x, float y, DimensionLine dimensionLine) {
    return x == dimensionLine.getXStart() && y == dimensionLine.getYStart()
           || x == dimensionLine.getXEnd() && y == dimensionLine.getYEnd();
  }

  /**
   * Paints an arc centered at <code>center</code> point that goes
   */
  private void paintAngleFeedback(Graphics2D g2D, Point2D center, 
                                  Point2D point1, Point2D point2,
                                  float planScale, Color selectionColor) {
    g2D.setColor(selectionColor);
    g2D.setStroke(new BasicStroke(1 / planScale));
    // Compute angles
    double angle1 = Math.atan2(center.getY() - point1.getY(), point1.getX() - center.getX());
    if (angle1 < 0) {
      angle1 = 2 * Math.PI + angle1; 
    }
    double angle2 = Math.atan2(center.getY() - point2.getY(), point2.getX() - center.getX());
    if (angle2 < 0) {
      angle2 = 2 * Math.PI + angle2; 
    }
    double extent = angle2 - angle1;
    if (angle1 > angle2) {
      extent = 2 * Math.PI + extent;
    }
    AffineTransform previousTransform = g2D.getTransform();
    // Draw an arc
    g2D.translate(center.getX(), center.getY());
    float radius = 20 / planScale;
    g2D.draw(new Arc2D.Double(-radius, -radius,
        radius * 2, radius * 2, Math.toDegrees(angle1), Math.toDegrees(extent), Arc2D.OPEN));
    // Draw two radius
    radius += 5 / planScale;
    g2D.draw(new Line2D.Double(0, 0, radius * Math.cos(angle1), -radius * Math.sin(angle1)));
    g2D.draw(new Line2D.Double(0, 0, radius * Math.cos(angle1 + extent), -radius * Math.sin(angle1 + extent)));
    g2D.setTransform(previousTransform);
  }

  /**
   * Paints the observer camera at its current location, if home camera is the observer camera.
   */
  private void paintCamera(Graphics2D g2D, List<Selectable> selectedItems,
                           Paint selectionOutlinePaint, Stroke selectionOutlineStroke, 
                           Paint indicatorPaint, float planScale, 
                           Color backgroundColor, Color foregroundColor) {
    ObserverCamera camera = this.home.getObserverCamera();
    if (camera == this.home.getCamera()) {
      AffineTransform previousTransform = g2D.getTransform();
      g2D.translate(camera.getX(), camera.getY());
      g2D.rotate(camera.getYaw());
  
      // Compute camera drawing at scale
      float [][] points = camera.getPoints();
      double yScale = Point2D.distance(points [0][0], points [0][1], points [3][0], points [3][1]);
      double xScale = Point2D.distance(points [0][0], points [0][1], points [1][0], points [1][1]);
      AffineTransform cameraTransform = AffineTransform.getScaleInstance(xScale, yScale);    
      Shape scaledCameraBody = 
          new Area(CAMERA_BODY).createTransformedArea(cameraTransform);
      Shape scaledCameraHead = 
          new Area(CAMERA_HEAD).createTransformedArea(cameraTransform);
      
      // Paint body
      g2D.setPaint(backgroundColor);
      g2D.fill(scaledCameraBody);
      g2D.setPaint(foregroundColor);
      BasicStroke stroke = new BasicStroke(getStrokeWidth(ObserverCamera.class, PaintMode.PAINT) / planScale);
      g2D.setStroke(stroke);
      g2D.draw(scaledCameraBody);
  
      if (selectedItems.contains(camera)
          && this.selectedItemsOutlinePainted) {
        g2D.setPaint(selectionOutlinePaint);
        g2D.setStroke(selectionOutlineStroke);
        Area cameraOutline = new Area(scaledCameraBody);
        cameraOutline.add(new Area(scaledCameraHead));
        g2D.draw(cameraOutline);      
      }
      
      // Paint head
      g2D.setPaint(backgroundColor);
      g2D.fill(scaledCameraHead);
      g2D.setPaint(foregroundColor);
      g2D.setStroke(stroke);
      g2D.draw(scaledCameraHead);
      // Paint field of sight angle
      double sin = (float)Math.sin(camera.getFieldOfView() / 2);
      double cos = (float)Math.cos(camera.getFieldOfView() / 2);
      float xStartAngle = (float)(0.9f * yScale * sin);
      float yStartAngle = (float)(0.9f * yScale * cos);
      float xEndAngle = (float)(2.2f * yScale * sin);
      float yEndAngle = (float)(2.2f * yScale * cos);
      GeneralPath cameraFieldOfViewAngle = new GeneralPath();
      cameraFieldOfViewAngle.moveTo(xStartAngle, yStartAngle);
      cameraFieldOfViewAngle.lineTo(xEndAngle, yEndAngle);
      cameraFieldOfViewAngle.moveTo(-xStartAngle, yStartAngle);
      cameraFieldOfViewAngle.lineTo(-xEndAngle, yEndAngle);
      g2D.draw(cameraFieldOfViewAngle);
      g2D.setTransform(previousTransform);
  
      // Paint resize indicator of selected camera
      if (selectedItems.size() == 1 
          && selectedItems.get(0) == camera) {
        paintCameraRotationIndicators(g2D, camera, indicatorPaint, planScale);
      }
    }
  }

  private void paintCameraRotationIndicators(Graphics2D g2D, 
                                             ObserverCamera camera, Paint indicatorPaint,
                                             float planScale) {
    if (this.resizeIndicatorVisible) {
      g2D.setPaint(indicatorPaint);
      g2D.setStroke(INDICATOR_STROKE);
      
      AffineTransform previousTransform = g2D.getTransform();
      // Draw yaw rotation indicator at middle of first and last point of camera 
      float [][] cameraPoints = camera.getPoints();
      float scaleInverse = 1 / planScale;
      g2D.translate((cameraPoints [0][0] + cameraPoints [3][0]) / 2, 
          (cameraPoints [0][1] + cameraPoints [3][1]) / 2);
      g2D.scale(scaleInverse, scaleInverse);
      g2D.rotate(camera.getYaw());
      g2D.draw(CAMERA_YAW_ROTATION_INDICATOR);
      g2D.setTransform(previousTransform);

      // Draw pitch rotation indicator at middle of second and third point of camera 
      g2D.translate((cameraPoints [1][0] + cameraPoints [2][0]) / 2, 
          (cameraPoints [1][1] + cameraPoints [2][1]) / 2);
      g2D.scale(scaleInverse, scaleInverse);
      g2D.rotate(camera.getYaw());
      g2D.draw(CAMERA_PITCH_ROTATION_INDICATOR);
      g2D.setTransform(previousTransform);

      // Draw elevation indicator at middle of first and second point of camera 
      g2D.translate((cameraPoints [0][0] + cameraPoints [1][0]) / 2, 
          (cameraPoints [0][1] + cameraPoints [1][1]) / 2);
      g2D.scale(scaleInverse, scaleInverse);
      g2D.draw(POINT_INDICATOR);
      g2D.translate(Math.sin(camera.getYaw()) * 8, -Math.cos(camera.getYaw()) * 8);
      g2D.draw(CAMERA_ELEVATION_INDICATOR);
      g2D.setTransform(previousTransform);
    }
  }

  /**
   * Paints rectangle feedback.
   */
  private void paintRectangleFeedback(Graphics2D g2D, Color selectionColor, float planScale) {
    if (this.rectangleFeedback != null) {
      g2D.setPaint(new Color(selectionColor.getRed(), selectionColor.getGreen(), selectionColor.getBlue(), 32));
      g2D.fill(this.rectangleFeedback);
      g2D.setPaint(selectionColor);
      g2D.setStroke(new BasicStroke(1 / planScale));
      g2D.draw(this.rectangleFeedback);
    }
  }

  /**
   * Returns the shape matching the coordinates in <code>points</code> array.
   */
  private Shape getShape(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 path;
  }

  /**
   * Sets rectangle selection feedback coordinates. 
   */
  public void setRectangleFeedback(float x0, float y0, float x1, float y1) {
    this.rectangleFeedback = new Rectangle2D.Float(x0, y0, 0, 0);
    this.rectangleFeedback.add(x1, y1);
    repaint();
  }
  
  /**
   * Ensures selected items are visible at screen and moves
   * scroll bars if needed.
   */
  public void makeSelectionVisible() {
    // As multiple selections may happen during an action, 
    // make the selection visible the latest possible to avoid multiple changes
    if (!this.selectionScrollUpdated) {
      this.selectionScrollUpdated = true;
      EventQueue.invokeLater(new Runnable() {
          public void run() {
            selectionScrollUpdated = false;
            Rectangle2D selectionBounds = getSelectionBounds(true);
            if (selectionBounds != null) {
              Rectangle pixelBounds = getShapePixelBounds(selectionBounds);
              pixelBounds.grow(5, 5);
              scrollRectToVisible(pixelBounds);
            }
          }
        });
    }
  }

  /**
   * Returns the bounds of the selected items.
   */
  private Rectangle2D getSelectionBounds(boolean includeCamera) {
    if (includeCamera) {
      return getItemsBounds(getGraphics(), this.home.getSelectedItems());
    } else {
      List<Selectable> selectedItems = new ArrayList<Selectable>(this.home.getSelectedItems());
      selectedItems.remove(this.home.getCamera());
      return getItemsBounds(getGraphics(), selectedItems);
    }
  }

  /**
   * Ensures the point at (<code>x</code>, <code>y</code>) is visible,
   * moving scroll bars if needed.
   */
  public void makePointVisible(float x, float y) {
    scrollRectToVisible(getShapePixelBounds(new Rectangle2D.Float(x, y, 1 / getScale(), 1 / getScale())));
  }

  /**
   * Moves the view from (dx, dy) unit in the scrolling zone it belongs to.
   */
  public void moveView(float dx, float dy) {
    if (getParent() instanceof JViewport) {
      JViewport viewport = (JViewport)getParent();
      Rectangle viewRectangle = viewport.getViewRect();
      viewRectangle.translate(Math.round(dx * getScale()), Math.round(dy * getScale()));
      viewRectangle.x = Math.min(Math.max(0, viewRectangle.x), getWidth() - viewRectangle.width);
      viewRectangle.y = Math.min(Math.max(0, viewRectangle.y), getHeight() - viewRectangle.height);
      viewport.setViewPosition(viewRectangle.getLocation());
    }
  }
  
  /**
   * Returns the scale used to display the plan.
   */
  public float getScale() {
    return this.scale;
  }

  /**
   * Sets the scale used to display the plan.
   * If this component is displayed in a viewport the view position is updated
   * to ensure the center's view will remain the same after the scale change.
   */
  public void setScale(float scale) {
    if (this.scale != scale) {
      JViewport parent = null;
      Rectangle viewRectangle = null;
      float xViewCenterPosition = 0;
      float yViewCenterPosition = 0;
      if (getParent() instanceof JViewport) {
        parent = (JViewport)getParent();
        viewRectangle = parent.getViewRect();
        xViewCenterPosition = convertXPixelToModel(viewRectangle.x + viewRectangle.width / 2);
        yViewCenterPosition = convertYPixelToModel(viewRectangle.y + viewRectangle.height / 2);
      }
      
      this.scale = scale;
      revalidate(false);

      if (parent instanceof JViewport) {
        Dimension viewSize = parent.getViewSize();
        float viewWidth = convertXPixelToModel(viewRectangle.x + viewRectangle.width) 
            - convertXPixelToModel(viewRectangle.x);          
        int xViewLocation = Math.max(0, Math.min(convertXModelToPixel(xViewCenterPosition - viewWidth / 2),
            viewSize.width - viewRectangle.x));
        float viewHeight = convertYPixelToModel(viewRectangle.y + viewRectangle.height) 
            - convertYPixelToModel(viewRectangle.y);          
        int yViewLocation = Math.max(0, Math.min(convertYModelToPixel(yViewCenterPosition - viewHeight / 2),
            viewSize.height - viewRectangle.y));        
        parent.setViewPosition(new Point(xViewLocation, yViewLocation));
      }
    }
  }

  /**
   * Returns <code>x</code> converted in model coordinates space.
   */
  public float convertXPixelToModel(int x) {
    Insets insets = getInsets();
    Rectangle2D planBounds = getPlanBounds();    
    return (x - insets.left) / getScale() - MARGIN + (float)planBounds.getMinX();
  }

  /**
   * Returns <code>y</code> converted in model coordinates space.
   */
  public float convertYPixelToModel(int y) {
    Insets insets = getInsets();
    Rectangle2D planBounds = getPlanBounds();    
    return (y - insets.top) / getScale() - MARGIN + (float)planBounds.getMinY();
  }

  /**
   * Returns <code>x</code> converted in view coordinates space.
   */
  private int convertXModelToPixel(float x) {
    Insets insets = getInsets();
    Rectangle2D planBounds = getPlanBounds();    
    return (int)Math.round((x - planBounds.getMinX() + MARGIN) * getScale()) + insets.left;
  }

  /**
   * Returns <code>y</code> converted in view coordinates space.
   */
  private int convertYModelToPixel(float y) {
    Insets insets = getInsets();
    Rectangle2D planBounds = getPlanBounds();    
    return (int)Math.round((y - planBounds.getMinY() + MARGIN) * getScale()) + insets.top;
  }

  /**
   * Returns <code>x</code> converted in screen coordinates space.
   */
  public int convertXModelToScreen(float x) {
    Point point = new Point(convertXModelToPixel(x), 0);
    SwingUtilities.convertPointToScreen(point, this);
    return point.x;
  }

  /**
   * Returns <code>y</code> converted in screen coordinates space.
   */
  public int convertYModelToScreen(float y) {
    Point point = new Point(0, convertYModelToPixel(y));
    SwingUtilities.convertPointToScreen(point, this);
    return point.y;
  }


  /**
   * Returns the length in centimeters of a pixel with the current scale.
   */
  public float getPixelLength() {
    return 1 / getScale();
  }
  
  /**
   * Returns the bounds of <code>shape</code> in pixels coordinates space.
   */
  private Rectangle getShapePixelBounds(Shape shape) {
    Rectangle2D shapeBounds = shape.getBounds2D();
    return new Rectangle(
        convertXModelToPixel((float)shapeBounds.getMinX()), 
        convertYModelToPixel((float)shapeBounds.getMinY()),
        (int)Math.round(shapeBounds.getWidth() * getScale()),
        (int)Math.round(shapeBounds.getHeight() * getScale()));
  }
  
  /**
   * Sets the cursor of this component as rotation cursor. 
   */
  public void setCursor(CursorType cursorType) {
    switch (cursorType) {
      case DRAW :
        setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
        break;
      case ROTATION :
        setCursor(this.rotationCursor);
        break;
      case HEIGHT :
        setCursor(this.heightCursor);
        break;
      case POWER :
        setCursor(this.powerCursor);
        break;
      case ELEVATION :
        setCursor(this.elevationCursor);
        break;
      case RESIZE :
        setCursor(this.resizeCursor);
        break;
      case PANNING :
        setCursor(this.panningCursor);
        break;
      case DUPLICATION :
        setCursor(this.duplicationCursor);
        break;
      case MOVE :
        setCursor(this.moveCursor);
        break;
      case SELECTION :
      default :
        setCursor(Cursor.getDefaultCursor());
        break;
    }
  }

  /**
   * Sets tool tip text displayed as feedback. 
   * @param toolTipFeedback the text displayed in the tool tip 
   *                    or <code>null</code> to make tool tip disappear.
   */
  public void setToolTipFeedback(String toolTipFeedback, float x, float y) {
    stopToolTipPropertiesEdition();
    JToolTip toolTip = getToolTip();
    // Change tool tip text    
    toolTip.setTipText(toolTipFeedback);    
    showToolTipComponentAt(toolTip, x , y);
  }

  /**
   * Returns the tool tip of the plan.
   */
  private JToolTip getToolTip() {
    // Create tool tip for this component
    if (this.toolTip == null) {
      this.toolTip = new JToolTip();
      this.toolTip.setComponent(this);
    }
    return this.toolTip;
  }

  /**
   * Shows the given component as a tool tip.
   */
  private void showToolTipComponentAt(JComponent toolTipComponent, float x, float y) {
    if (this.toolTipWindow == null) {
      // Show tool tip in a window (we don't use a Swing Popup because 
      // we require the tool tip window to move along with mouse pointer 
      // and a Swing popup can't move without hiding then showing it again)
      this.toolTipWindow = new JWindow(JOptionPane.getFrameForComponent(this));
      this.toolTipWindow.setFocusableWindowState(false);
      this.toolTipWindow.add(toolTipComponent);
      // Add to window a mouse listener that redispatch mouse events to
      // plan component (if the user moves fast enough the mouse pointer in a way 
      // it's in toolTipWindow, the matching event is dispatched to toolTipWindow)
      MouseInputAdapter mouseAdapter = new MouseInputAdapter() {
        @Override
        public void mousePressed(MouseEvent ev) {
          mouseMoved(ev);
        }
        
        @Override
        public void mouseReleased(MouseEvent ev) {
          mouseMoved(ev);
        }
        
        @Override
        public void mouseMoved(MouseEvent ev) {
          dispatchEvent(SwingUtilities.convertMouseEvent(toolTipWindow, ev, PlanComponent.this));
        }
        
        @Override
        public void mouseDragged(MouseEvent ev) {
          mouseMoved(ev);
        }
      };
      this.toolTipWindow.addMouseListener(mouseAdapter);
      this.toolTipWindow.addMouseMotionListener(mouseAdapter);
    } else {
      Container contentPane = this.toolTipWindow.getContentPane();
      if (contentPane.getComponent(0) != toolTipComponent) {
        contentPane.removeAll();
        contentPane.add(toolTipComponent);
      }
      toolTipComponent.revalidate();
    }
    // Convert (x, y) to screen coordinates 
    Point point = new Point(convertXModelToPixel(x), convertYModelToPixel(y));
    SwingUtilities.convertPointToScreen(point, this);
    // Add to point the half of cursor size
    Dimension cursorSize = getToolkit().getBestCursorSize(16, 16);
    if (cursorSize.width != 0) {
      point.x += cursorSize.width / 2 + 3;
      point.y += cursorSize.height / 2 + 3;
    } else {
      // If custom cursor isn't supported let's consider 
      // default cursor size is 16 pixels wide
      point.x += 11;
      point.y += 11;
    }
    this.toolTipWindow.setLocation(point);      
    this.toolTipWindow.pack();
    this.toolTipWindow.setVisible(true);
    toolTipComponent.paintImmediately(toolTipComponent.getBounds());
  }
  
  /**
   * Set tool tip edition.
   */
  public void setToolTipEditedProperties(final PlanController.EditableProperty [] toolTipEditedProperties, 
                                         Object [] toolTipPropertyValues,
                                         float x, float y) {
    final JPanel toolTipPropertiesPanel = new JPanel(new GridBagLayout());
    // Reuse tool tip look
    Border border = UIManager.getBorder("ToolTip.border");
    if (!OperatingSystem.isMacOSX()) {
      border = BorderFactory.createCompoundBorder(border, BorderFactory.createEmptyBorder(0, 3, 0, 2));
    }
    toolTipPropertiesPanel.setBorder(border);
    // Copy colors from tool tip instance (on Linux, colors aren't set in UIManager)
    JToolTip toolTip = getToolTip();
    toolTipPropertiesPanel.setBackground(toolTip.getBackground());
    toolTipPropertiesPanel.setForeground(toolTip.getForeground());

    // Add labels and text fields to tool tip panel
    for (int i = 0; i < toolTipEditedProperties.length; i++) {
      JFormattedTextField textField = this.toolTipEditableTextFields.get(toolTipEditedProperties [i]);
      textField.setValue(toolTipPropertyValues [i]); 
      JLabel label = new JLabel(this.preferences.getLocalizedString(PlanComponent.class, 
          toolTipEditedProperties [i].name() + ".editablePropertyLabel.text") + " ");
      label.setFont(textField.getFont());
      JLabel unitLabel = null;
      if (toolTipEditedProperties [i] == PlanController.EditableProperty.ANGLE
          || toolTipEditedProperties [i] == PlanController.EditableProperty.ARC_EXTENT) {
        unitLabel = new JLabel(this.preferences.getLocalizedString(PlanComponent.class, "degreeLabel.text"));
      } else if (this.preferences.getLengthUnit() != LengthUnit.INCH
                 || this.preferences.getLengthUnit() != LengthUnit.INCH_DECIMALS) {
        unitLabel = new JLabel(" " + this.preferences.getLengthUnit().getName());
      }
      
      JPanel labelTextFieldPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0));
      labelTextFieldPanel.setOpaque(false);
      
      labelTextFieldPanel.add(label);
      labelTextFieldPanel.add(textField);
      if (unitLabel != null) {
        unitLabel.setFont(textField.getFont());
        labelTextFieldPanel.add(unitLabel);
      }
      toolTipPropertiesPanel.add(labelTextFieldPanel, new GridBagConstraints(
          0, i, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE,
          new Insets(0, 0, 0, 0), 0, 0));
    }
    
    showToolTipComponentAt(toolTipPropertiesPanel, x, y);
    // Add a key listener that redispatches events to tool tip text fields
    // (don't give focus to tool tip window otherwise plan component window will lose focus)
    this.toolTipKeyListener = new KeyListener() {
        private int focusedTextFieldIndex;
        private JTextComponent focusedTextField;
  
        {
          // Simulate focus on first text field
          setFocusedTextFieldIndex(0);
        }
        
        private void setFocusedTextFieldIndex(int textFieldIndex) {
          if (this.focusedTextField != null) {
            this.focusedTextField.getCaret().setVisible(false);
            this.focusedTextField.getCaret().setSelectionVisible(false);        
          }
          this.focusedTextFieldIndex = textFieldIndex;
          this.focusedTextField = toolTipEditableTextFields.get(toolTipEditedProperties [textFieldIndex]);
          if (this.focusedTextField.getText().length() == 0) {
            this.focusedTextField.getCaret().setVisible(false);
          } else {
            this.focusedTextField.selectAll();
          }
          this.focusedTextField.getCaret().setSelectionVisible(true);
        }
        
        public void keyPressed(KeyEvent ev) {
          keyTyped(ev);
        }
  
        public void keyReleased(KeyEvent ev) {
          if (ev.getKeyCode() != KeyEvent.VK_CONTROL
              && ev.getKeyCode() != KeyEvent.VK_ALT) { 
            // Forward other key events to focused text field (except for Ctrl and Alt key, otherwise InputMap won't receive it)
            KeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(this.focusedTextField, ev);
          }
        }
  
        public void keyTyped(KeyEvent ev) {
          Set<AWTKeyStroke> forwardKeys = this.focusedTextField.getFocusTraversalKeys(
              KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS);
          if (forwardKeys.contains(AWTKeyStroke.getAWTKeyStrokeForEvent(ev))
              || ev.getKeyCode() == KeyEvent.VK_DOWN) {
            setFocusedTextFieldIndex((this.focusedTextFieldIndex + 1) % toolTipEditedProperties.length);
            ev.consume();
          } else {
            Set<AWTKeyStroke> backwardKeys = this.focusedTextField.getFocusTraversalKeys(
                KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS);
            if (backwardKeys.contains(AWTKeyStroke.getAWTKeyStrokeForEvent(ev))
                || ev.getKeyCode() == KeyEvent.VK_UP) {
              setFocusedTextFieldIndex((this.focusedTextFieldIndex - 1 + toolTipEditedProperties.length) % toolTipEditedProperties.length);
              ev.consume();
            } else if ((ev.getKeyCode() == KeyEvent.VK_HOME
                          || ev.getKeyCode() == KeyEvent.VK_END)
                       && OperatingSystem.isMacOSX()
                       && !OperatingSystem.isMacOSXLeopardOrSuperior()) {
              // Support Home and End keys under Mac OS X Tiger
              if (ev.getKeyCode() == KeyEvent.VK_HOME) {
                focusedTextField.setCaretPosition(0);
              } else if (ev.getKeyCode() == KeyEvent.VK_END) {
                focusedTextField.setCaretPosition(focusedTextField.getText().length());
              }
              ev.consume();
            } else if (ev.getKeyCode() != KeyEvent.VK_ESCAPE
                       && ev.getKeyCode() != KeyEvent.VK_CONTROL
                       && ev.getKeyCode() != KeyEvent.VK_ALT) { 
              // Forward other key events to focused text field (except for Esc key, otherwise InputMap won't receive it)
              KeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(this.focusedTextField, ev);
              this.focusedTextField.getCaret().setVisible(true);
              toolTipWindow.pack();
            } 
          }
        }      
      };

    addKeyListener(this.toolTipKeyListener);
    setFocusTraversalKeysEnabled(false);
    installEditionKeyboardActions();
  }
  
  /**
   * Deletes tool tip text from screen. 
   */
  public void deleteToolTipFeedback() {
    stopToolTipPropertiesEdition();
    if (this.toolTip != null) {
      this.toolTip.setTipText(null);
    }
    if (this.toolTipWindow != null) {
      this.toolTipWindow.setVisible(false);
    }
  }

  /**
   * Stops editing in tool tip text fields.
   */
  private void stopToolTipPropertiesEdition() {
    if (this.toolTipKeyListener != null) {
      installDefaultKeyboardActions();
      setFocusTraversalKeysEnabled(true);
      removeKeyListener(toolTipKeyListener);
      this.toolTipKeyListener = null;
      
      for (JFormattedTextField textField : this.toolTipEditableTextFields.values()) {
        textField.getCaret().setVisible(false);
        textField.getCaret().setSelectionVisible(false);
      }      
    }
  }

  /**
   * Sets whether the resize indicator of selected wall or piece of furniture 
   * should be visible or not. 
   */
  public void setResizeIndicatorVisible(boolean resizeIndicatorVisible) {
    this.resizeIndicatorVisible = resizeIndicatorVisible;    
    repaint();
  }
  
  /**
   * Sets the location point for alignment feedback. 
   */
  public void setAlignmentFeedback(Class<? extends Selectable> alignedObjectClass,
                                   Selectable alignedObject,
                                   float x, 
                                   float y, 
                                   boolean showPointFeedback) {
    this.alignedObjectClass = alignedObjectClass;
    this.alignedObjectFeedback = alignedObject;
    this.locationFeeback = new Point2D.Float(x, y);
    this.showPointFeedback = showPointFeedback;
    repaint();
  }
  
  /**
   * Sets the points used to draw an angle in plan view.
   */
  public void setAngleFeedback(float xCenter, float yCenter, 
                               float x1, float y1, 
                               float x2, float y2) {
    this.centerAngleFeedback = new Point2D.Float(xCenter, yCenter);
    this.point1AngleFeedback = new Point2D.Float(x1, y1);
    this.point2AngleFeedback = new Point2D.Float(x2, y2);
  }

  /**
   * Sets the feedback of dragged items drawn during a drag and drop operation, 
   * initiated from outside of plan view.
   */
  public void setDraggedItemsFeedback(List<Selectable> draggedItems) {
    this.draggedItemsFeedback = draggedItems;
    repaint();
  }

  /**
   * Sets the given dimension lines to be drawn as feedback.
   */
  public void setDimensionLinesFeedback(List<DimensionLine> dimensionLines) {
    this.dimensionLinesFeedback = dimensionLines;
    repaint();
  }

  /**
   * Deletes all elements shown as feedback.
   */
  public void deleteFeedback() {
    deleteToolTipFeedback();
    this.rectangleFeedback = null;
    
    this.alignedObjectClass = null;
    this.alignedObjectFeedback = null;
    this.locationFeeback = null;

    this.centerAngleFeedback = null;
    this.point1AngleFeedback = null;
    this.point2AngleFeedback = null;

    this.draggedItemsFeedback = null;

    this.dimensionLinesFeedback = null;
    repaint();
  }

  /**
   * Returns <code>true</code>.
   */
  public boolean canImportDraggedItems(List<Selectable> items, int x, int y) {
    return true;
  }

  // Scrollable implementation
  public Dimension getPreferredScrollableViewportSize() {
    return getPreferredSize();
  }

  public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
    if (orientation == SwingConstants.HORIZONTAL) {
      return visibleRect.width / 2;
    } else { // SwingConstants.VERTICAL
      return visibleRect.height / 2;
    }
  }

  public boolean getScrollableTracksViewportHeight() {
    // Return true if the plan's preferred height is smaller than the viewport height
    return getParent() instanceof JViewport
        && getPreferredSize().height < ((JViewport)getParent()).getHeight();
  }

  public boolean getScrollableTracksViewportWidth() {
    // Return true if the plan's preferred width is smaller than the viewport width
    return getParent() instanceof JViewport
        && getPreferredSize().width < ((JViewport)getParent()).getWidth();
  }

  public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
    if (orientation == SwingConstants.HORIZONTAL) {
      return visibleRect.width / 10;
    } else { // SwingConstants.VERTICAL
      return visibleRect.height / 10;
    }
  }
  
  /**
   * Returns the component used as an horizontal ruler for this plan.
   */
  public View getHorizontalRuler() {
    if (this.horizontalRuler == null) {
      this.horizontalRuler = new PlanRulerComponent(SwingConstants.HORIZONTAL);
    } 
    return this.horizontalRuler;
  }
  
  /**
   * Returns the component used as a vertical ruler for this plan.
   */
  public View getVerticalRuler() {
    if (this.verticalRuler == null) {
      this.verticalRuler = new PlanRulerComponent(SwingConstants.VERTICAL);
    } 
    return this.verticalRuler;
  }
  
  /**
   * A component displaying the plan horizontal or vertical ruler associated to this plan.
   */
  public class PlanRulerComponent extends JComponent implements View {
    private int   orientation;
    private Point mouseLocation;

    /**
     * Creates a plan ruler.
     * @param orientation <code>SwingConstants.HORIZONTAL</code> or 
     *                    <code>SwingConstants.VERTICAL</code>. 
     */
    public PlanRulerComponent(int orientation) {
      this.orientation = orientation;
      setOpaque(true);
      // Use same font as tool tips
      setFont(UIManager.getFont("ToolTip.font"));
      addMouseListeners();
    }

    /**
     * Adds a mouse listener to this ruler that stores current mouse location. 
     */
    private void addMouseListeners() {
      MouseInputListener mouseInputListener = new MouseInputAdapter() {
          @Override
          public void mouseDragged(MouseEvent ev) {
            mouseLocation = ev.getPoint();
            repaint();
          }
  
          @Override
          public void mouseMoved(MouseEvent ev) {
            mouseLocation = ev.getPoint();
            repaint();
          }

          @Override
          public void mouseEntered(MouseEvent ev) {
            mouseLocation = ev.getPoint();
            repaint();
          }
          
          @Override
          public void mouseExited(MouseEvent ev) {
            mouseLocation = null;
            repaint();
          }
        };
      PlanComponent.this.addMouseListener(mouseInputListener);
      PlanComponent.this.addMouseMotionListener(mouseInputListener);
      addAncestorListener(new AncestorListener() {
          public void ancestorAdded(AncestorEvent ev) {
            removeAncestorListener(this);
            if (getParent() instanceof JViewport) {
              ((JViewport)getParent()).addChangeListener(new ChangeListener() {
                  public void stateChanged(ChangeEvent ev) {
                    mouseLocation = MouseInfo.getPointerInfo().getLocation(); 
                    SwingUtilities.convertPointFromScreen(mouseLocation, PlanComponent.this);
                    repaint();
                  }
                });
            }
          }
          
          public void ancestorRemoved(AncestorEvent ev) {
          }
          
          public void ancestorMoved(AncestorEvent ev) {
          }
        });
    }

    /**
     * Returns the preferred size of this component.
     */
    @Override
    public Dimension getPreferredSize() {
      if (isPreferredSizeSet()) {
        return super.getPreferredSize();
      } else {
        Insets insets = getInsets();
        Rectangle2D planBounds = getPlanBounds();
        FontMetrics metrics = getFontMetrics(getFont());
        int ruleHeight = metrics.getAscent() + 6;
        if (this.orientation == SwingConstants.HORIZONTAL) {
          return new Dimension(
              Math.round(((float)planBounds.getWidth() + MARGIN * 2)
                         * getScale()) + insets.left + insets.right,
              ruleHeight);
        } else {
          return new Dimension(ruleHeight,
              Math.round(((float)planBounds.getHeight() + MARGIN * 2)
                         * getScale()) + insets.top + insets.bottom);
        }
      }
    }
    
    /**
     * Paints this component.
     */
    @Override
    protected void paintComponent(Graphics g) {
      Graphics2D g2D = (Graphics2D)g.create();
      paintBackground(g2D);
      Insets insets = getInsets();
      // Clip component to avoid drawing in empty borders
      g2D.clipRect(insets.left, insets.top, 
          getWidth() - insets.left - insets.right, 
          getHeight() - insets.top - insets.bottom);
      // Change component coordinates system to plan system
      Rectangle2D planBounds = getPlanBounds();    
      float paintScale = getScale();
      g2D.translate(insets.left + (MARGIN - planBounds.getMinX()) * paintScale,
          insets.top + (MARGIN - planBounds.getMinY()) * paintScale);
      g2D.scale(paintScale, paintScale);
      g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
      // Paint component contents
      paintRuler(g2D, paintScale);
      g2D.dispose();
    }

    /**
     * Fills the background with UI window background color. 
     */
    private void paintBackground(Graphics2D g2D) {
      if (isOpaque()) {
        g2D.setColor(getBackground());
        g2D.fillRect(0, 0, getWidth(), getHeight());
      }
    }

    /**
     * Paints background grid lines.
     */
    private void paintRuler(Graphics2D g2D, float rulerScale) {
      float gridSize = getGridSize(rulerScale);
      float mainGridSize = getMainGridSize(rulerScale);
      
      float xMin;
      float yMin;
      float xMax;
      float yMax;
      float xRulerBase;
      float yRulerBase;
      Rectangle2D planBounds = getPlanBounds();    
      boolean leftToRightOriented = getComponentOrientation().isLeftToRight();
      if (getParent() instanceof JViewport) {
        Rectangle viewRectangle = ((JViewport)getParent()).getViewRect();
        xMin = convertXPixelToModel(viewRectangle.x - 1);
        yMin = convertYPixelToModel(viewRectangle.y - 1);
        xMax = convertXPixelToModel(viewRectangle.x + viewRectangle.width);
        yMax = convertYPixelToModel(viewRectangle.y + viewRectangle.height);
        xRulerBase = leftToRightOriented 
            ? convertXPixelToModel(viewRectangle.x + viewRectangle.width - 1)
            : convertXPixelToModel(viewRectangle.x); 
        yRulerBase = convertYPixelToModel(viewRectangle.y + viewRectangle.height - 1);
      } else {
        xMin = (float)planBounds.getMinX() - MARGIN;
        yMin = (float)planBounds.getMinY() - MARGIN;
        xMax = convertXPixelToModel(getWidth() - 1);
        yRulerBase = 
        yMax = convertYPixelToModel(getHeight() - 1);
        xRulerBase = leftToRightOriented ? xMax : xMin;
      }

      FontMetrics metrics = getFontMetrics(getFont());
      int fontAscent = metrics.getAscent();
      float tickSize = 5 / rulerScale;
      float mainTickSize = (fontAscent + 6) / rulerScale;
      NumberFormat format = NumberFormat.getIntegerInstance();
      
      g2D.setColor(getForeground());
      float lineWidth = 0.5f / rulerScale;
      g2D.setStroke(new BasicStroke(lineWidth));
      if (this.orientation == SwingConstants.HORIZONTAL) {
        // Draw horizontal ruler base
        g2D.draw(new Line2D.Float(xMin, yRulerBase - lineWidth / 2, xMax, yRulerBase - lineWidth  / 2));
        // Draw small ticks
        for (double x = (int)(xMin / gridSize) * gridSize; x < xMax; x += gridSize) {
          g2D.draw(new Line2D.Double(x, yMax - tickSize, x, yMax));
        }
      } else {
        // Draw vertical ruler base
        if (leftToRightOriented) {
          g2D.draw(new Line2D.Float(xRulerBase - lineWidth / 2, yMin, xRulerBase - lineWidth / 2, yMax));
        } else {
          g2D.draw(new Line2D.Float(xRulerBase + lineWidth / 2, yMin, xRulerBase + lineWidth / 2, yMax));
        }
        // Draw small ticks
        for (double y = (int)(yMin / gridSize) * gridSize; y < yMax; y += gridSize) {
          if (leftToRightOriented) {
            g2D.draw(new Line2D.Double(xMax - tickSize, y, xMax, y));
          } else {
            g2D.draw(new Line2D.Double(xMin, y, xMin + tickSize, y));
          }
        }
      }

      if (mainGridSize != gridSize) {
        g2D.setStroke(new BasicStroke(1.5f / rulerScale,
            BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
        AffineTransform previousTransform = g2D.getTransform();
        // Draw big ticks
        if (this.orientation == SwingConstants.HORIZONTAL) {
          for (double x = ((int)(xMin / mainGridSize) - 1) * mainGridSize; x < xMax; x += mainGridSize) {
            g2D.draw(new Line2D.Double(x, yMax - mainTickSize, x, yMax));
            // Draw unit text
            g2D.translate(x, yMax - mainTickSize);
            g2D.scale(1 / rulerScale, 1 / rulerScale);
            g2D.drawString(getFormattedTickText(format, x), 3, fontAscent - 1);
            g2D.setTransform(previousTransform);
          }
        } else {
          for (double y = ((int)(yMin / mainGridSize) - 1) * mainGridSize; y < yMax; y += mainGridSize) {
            String yText = getFormattedTickText(format, y);
            if (leftToRightOriented) {
              g2D.draw(new Line2D.Double(xMax - mainTickSize, y, xMax, y));
              // Draw unit text with a vertical orientation
              g2D.translate(xMax - mainTickSize, y);
              g2D.scale(1 / rulerScale, 1 / rulerScale);
              g2D.rotate(-Math.PI / 2);
              g2D.drawString(yText, -metrics.stringWidth(yText) - 3, fontAscent - 1);
            } else {
              g2D.draw(new Line2D.Double(xMin, y, xMin +  mainTickSize, y));
              // Draw unit text with a vertical orientation
              g2D.translate(xMin + mainTickSize, y);
              g2D.scale(1 / rulerScale, 1 / rulerScale);
              g2D.rotate(Math.PI / 2);
              g2D.drawString(yText, 3, fontAscent - 1);
            }
            g2D.setTransform(previousTransform);
          }
        }
      }

      if (this.mouseLocation != null) {
        g2D.setColor(getSelectionColor());
        g2D.setStroke(new BasicStroke(1 / rulerScale));
        if (this.orientation == SwingConstants.HORIZONTAL) {
          // Draw mouse feedback vertical line
          float x = convertXPixelToModel(this.mouseLocation.x);
          g2D.draw(new Line2D.Float(x, yMax - mainTickSize, x, yMax));
        } else {
          // Draw mouse feedback horizontal line
          float y = convertYPixelToModel(this.mouseLocation.y);
          if (leftToRightOriented) {
            g2D.draw(new Line2D.Float(xMax - mainTickSize, y, xMax, y));
          } else {
            g2D.draw(new Line2D.Float(xMin, y, xMin + mainTickSize, y));
          }
        }
      }
    }

    private String getFormattedTickText(NumberFormat format, double value) {
      String text;
      if (Math.abs(value) < 1E-5) {
        value = 0; // Avoid "-0" text
      }
      LengthUnit lengthUnit = preferences.getLengthUnit();
      if (lengthUnit == LengthUnit.INCH 
          || lengthUnit == LengthUnit.INCH_DECIMALS) {
        text = format.format(LengthUnit.centimeterToFoot((float)value)) + "'"; 
      } else {
        text = format.format(value / 100);
        if (value == 0) {
          text += LengthUnit.METER.getName();
        }
      }
      return text;
    }
  }

  /**
   * A proxy for the furniture icon seen from top. 
   */
  private abstract static class PieceOfFurnitureTopViewIcon implements Icon {
    private Icon icon;
    
    public PieceOfFurnitureTopViewIcon(Icon icon) {
      this.icon = icon;
    }

    public int getIconWidth() {
      return this.icon.getIconWidth();
    }

    public int getIconHeight() {
      return this.icon.getIconHeight();
    }
    
    public void paintIcon(Component c, Graphics g, int x, int y) {
      this.icon.paintIcon(c, g, x, y);
    }

    public boolean isWaitIcon() {
      return IconManager.getInstance().isWaitIcon(this.icon);
    }
    
    public boolean isErrorIcon() {
      return IconManager.getInstance().isErrorIcon(this.icon);
    }
    
    protected void setIcon(Icon icon) {
      this.icon = icon;
    }
  }
  
  /**
   * A proxy for the furniture plan icon generated from its plan icon. 
   */
  private static class PieceOfFurniturePlanIcon extends PieceOfFurnitureTopViewIcon {
    private final float pieceWidth;
    private final float pieceDepth;
    private Integer     pieceColor;
    private HomeTexture pieceTexture;
    
    /**
     * Creates a plan icon proxy for a <code>piece</code> of furniture.
     * @param piece an object containing a plan icon content
     * @param waitingComponent a waiting component. If <code>null</code>, the returned icon will
     *            be read immediately in the current thread.
     */
    public PieceOfFurniturePlanIcon(final HomePieceOfFurniture piece, 
                                    final Component waitingComponent) {
      super(IconManager.getInstance().getIcon(piece.getPlanIcon(), waitingComponent));
      this.pieceWidth = piece.getWidth();
      this.pieceDepth = piece.getDepth();
      this.pieceColor = piece.getColor();
      this.pieceTexture = piece.getTexture();
    }
    
    @Override
    public void paintIcon(final Component c, Graphics g, int x, int y) {
      if (!isWaitIcon()
          && !isErrorIcon()) {
        if (this.pieceColor != null) {
          // Create a monochrome icon from plan icon  
          BufferedImage image = new BufferedImage(getIconWidth(), getIconHeight(), BufferedImage.TYPE_INT_ARGB);
          Graphics imageGraphics = image.getGraphics();
          super.paintIcon(c, imageGraphics, 0, 0);
          imageGraphics.dispose();
          
          final int colorRed   = this.pieceColor & 0xFF0000;
          final int colorGreen = this.pieceColor & 0xFF00;
          final int colorBlue  = this.pieceColor & 0xFF;
          setIcon(new ImageIcon(c.createImage(new FilteredImageSource(image.getSource (),
              new RGBImageFilter() {
                {
                  canFilterIndexColorModel = true;
                }
  
                public int filterRGB (int x, int y, int argb) {
                  int alpha = argb & 0xFF000000;
                  int red   = (argb & 0x00FF0000) >> 16;
                  int green = (argb & 0x0000FF00) >> 8;
                  int blue  = argb & 0x000000FF;
                  
                  // Approximate brightness computation to 0.375 red + 0.5 green + 0.125 blue 
                  // for faster results
                  int brightness = ((red + red + red + green + green + green + green + blue) >> 4) + 0x7F;
                  
                  red   = (colorRed   * brightness / 0xFF) & 0xFF0000;
                  green = (colorGreen * brightness / 0xFF) & 0xFF00;
                  blue  = (colorBlue  * brightness / 0xFF) & 0xFF;
                  return alpha | red | green | blue;
                }
              }))));
          // Don't need color information anymore
          this.pieceColor = null;
        } else if (this.pieceTexture != null) {
          if (isTextureManagerAvailable()) {
            // Prefer to share textures images with texture manager if it's available
            TextureManager.getInstance().loadTexture(this.pieceTexture.getImage(), true,
                new TextureManager.TextureObserver() {
                  public void textureUpdated(Texture texture) {                  
                    setTexturedIcon(c, ((ImageComponent2D)texture.getImage(0)).getImage());
                  }
                });
          } else { 
            Icon textureIcon = IconManager.getInstance().getIcon(this.pieceTexture.getImage(), null);
            if (IconManager.getInstance().isErrorIcon(textureIcon)) {
              setTexturedIcon(c, ERROR_TEXTURE_IMAGE);                    
            } else {
              BufferedImage textureIconImage = new BufferedImage(
                  textureIcon.getIconWidth(), textureIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
              Graphics2D g2DIcon = (Graphics2D)textureIconImage.getGraphics();
              textureIcon.paintIcon(c, g2DIcon, 0, 0);
              g2DIcon.dispose();
              setTexturedIcon(c, textureIconImage);
            }
          }
  
          // Don't need texture information anymore
          this.pieceTexture = null;
        }
      }
      super.paintIcon(c, g, x, y);
    }
    
    private void setTexturedIcon(Component c, BufferedImage textureImage) {
      // Paint plan icon in an image
      BufferedImage image = new BufferedImage(getIconWidth(), getIconHeight(), BufferedImage.TYPE_INT_ARGB);
      final Graphics2D imageGraphics = (Graphics2D)image.getGraphics();
      PieceOfFurniturePlanIcon.super.paintIcon(c, imageGraphics, 0, 0);                  
      // Fill the pixels of plan icon with texture image
      imageGraphics.setPaint(new TexturePaint(textureImage, 
          new Rectangle2D.Float(0, 0, -getIconWidth() / pieceWidth * pieceTexture.getWidth(), 
              -getIconHeight() / pieceDepth * pieceTexture.getHeight())));
      imageGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN));
      imageGraphics.fillRect(0, 0, getIconWidth(), getIconHeight());                  
      imageGraphics.dispose();
      
      setIcon(new ImageIcon(image));
    }
  }
  
  /**
   * A proxy for the furniture top view icon generated from its 3D model. 
   */
  private static class PieceOfFurnitureModelIcon extends PieceOfFurnitureTopViewIcon {
    private static Canvas3D        canvas3D;
    private static BranchGroup     sceneRoot;
    private static ExecutorService iconsCreationExecutor;
    
    static {
      // Create the universe used to compute top view icons 
      canvas3D = Component3DManager.getInstance().getOffScreenCanvas3D(128, 128);
      SimpleUniverse universe = new SimpleUniverse(canvas3D);
      ViewingPlatform viewingPlatform = universe.getViewingPlatform();
      // View model from top
      TransformGroup viewPlatformTransform = viewingPlatform.getViewPlatformTransform();
      Transform3D rotation = new Transform3D();
      rotation.rotX(-Math.PI / 2);
      Transform3D transform = new Transform3D();
      transform.setTranslation(new Vector3f(0, 5, 0));
      transform.mul(rotation);
      viewPlatformTransform.setTransform(transform);
      // Use parallel projection
      Viewer viewer = viewingPlatform.getViewers() [0];      
      javax.media.j3d.View view = viewer.getView();
      view.setProjectionPolicy(javax.media.j3d.View.PARALLEL_PROJECTION);
      sceneRoot = new BranchGroup();
      // Prepare scene root
      sceneRoot.setCapability(BranchGroup.ALLOW_CHILDREN_READ);
      sceneRoot.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
      sceneRoot.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
      Background background = new Background(1.1f, 1.1f, 1.1f);
      background.setCapability(Background.ALLOW_COLOR_WRITE);
      background.setApplicationBounds(new BoundingBox(new Point3d(-1.1, -1.1, -1.1), new Point3d(1.1, 1.1, 1.1)));
      sceneRoot.addChild(background);
      Light [] lights = {new DirectionalLight(new Color3f(0.6f, 0.6f, 0.6f), new Vector3f(1.5f, -0.8f, -1)),         
                         new DirectionalLight(new Color3f(0.6f, 0.6f, 0.6f), new Vector3f(-1.5f, -0.8f, -1)), 
                         new DirectionalLight(new Color3f(0.6f, 0.6f, 0.6f), new Vector3f(0, -0.8f, 1)), 
                         new AmbientLight(new Color3f(0.2f, 0.2f, 0.2f))};
      for (Light light : lights) {
        light.setInfluencingBounds(new BoundingBox(new Point3d(-1.1, -1.1, -1.1), new Point3d(1.1, 1.1, 1.1)));
        sceneRoot.addChild(light);
      }
      universe.addBranchGraph(sceneRoot);
      iconsCreationExecutor = Executors.newSingleThreadExecutor();
    }
    
    /**
     * Creates a top view icon proxy for a <code>piece</code> of furniture.
     * @param piece an object containing a 3D content
     * @param waitingComponent a waiting component. If <code>null</code>, the returned icon will
     *            be read immediately in the current thread.
     */
    public PieceOfFurnitureModelIcon(final HomePieceOfFurniture piece, 
                                     final Component waitingComponent) {
      super(IconManager.getInstance().getWaitIcon());
      ModelManager.getInstance().loadModel(piece.getModel(), waitingComponent == null,
          new ModelManager.ModelObserver() {
            public void modelUpdated(final BranchGroup modelNode) {
              // Now that it's sure that 3D model exists
              // work on a clone of the piece centered at the origin
              // with the same size to get a correct texture mapping
              final HomePieceOfFurniture normalizedPiece = piece.clone();
              if (normalizedPiece.isResizable()) {
                normalizedPiece.setModelMirrored(false);
              }
              normalizedPiece.setX(0);
              normalizedPiece.setY(0);
              normalizedPiece.setElevation(-normalizedPiece.getHeight() / 2);
              normalizedPiece.setAngle(0);
              final float pieceWidth = normalizedPiece.getWidth();
              final float pieceDepth = normalizedPiece.getDepth();
              final float pieceHeight = normalizedPiece.getHeight();
              if (waitingComponent != null) {
                // Generate icons in an other thread to avoid blocking EDT during offscreen rendering
                iconsCreationExecutor.execute(new Runnable() {
                    public void run() {
                      setIcon(createIcon(new HomePieceOfFurniture3D(normalizedPiece, null, true, true),
                          pieceWidth, pieceDepth, pieceHeight));
                      waitingComponent.repaint();
                    }
                  });
              } else {
                setIcon(createIcon(new HomePieceOfFurniture3D(normalizedPiece, null, true, true),
                    pieceWidth, pieceDepth, pieceHeight));
              }
            }
        
            public void modelError(Exception ex) {
              // Too bad, we'll use errorIcon
              setIcon(IconManager.getInstance().getErrorIcon());                
              if (waitingComponent != null) {
                waitingComponent.repaint();
              }
            }
          });
    }
    
    /**
     * Returns an icon created and scaled from piece model content.
     */
    private Icon createIcon(BranchGroup modelNode,  
                            float pieceWidth, float pieceDepth, float pieceHeight) {
      // Add piece model scene to a normalized transform group
      Transform3D scaleTransform = new Transform3D();
      scaleTransform.setScale(new Vector3d(2 / pieceWidth, 2 / pieceHeight, 2 / pieceDepth));
      TransformGroup modelTransformGroup = new TransformGroup();
      modelTransformGroup.setTransform(scaleTransform);
      modelTransformGroup.addChild(modelNode);
      // Replace model textures by clones because Java 3D doesn't accept all the time 
      // to share textures between offscreen and onscreen environments 
      cloneTexture(modelNode, new HashMap<Texture, Texture>());

      BranchGroup model = new BranchGroup();
      model.setCapability(BranchGroup.ALLOW_DETACH);
      model.addChild(modelTransformGroup);
      sceneRoot.addChild(model);
      
      // Render scene with a white background
      Background background = (Background)sceneRoot.getChild(0);        
      background.setColor(1, 1, 1);
      canvas3D.renderOffScreenBuffer();
      canvas3D.waitForOffScreenRendering();          
      BufferedImage imageWithWhiteBackgound = canvas3D.getOffScreenBuffer().getImage();
      int [] imageWithWhiteBackgoundPixels = getImagePixels(imageWithWhiteBackgound);
      
      // Render scene with a black background
      background.setColor(0, 0, 0);
      canvas3D.renderOffScreenBuffer();
      canvas3D.waitForOffScreenRendering();          
      BufferedImage imageWithBlackBackgound = canvas3D.getOffScreenBuffer().getImage();
      int [] imageWithBlackBackgoundPixels = getImagePixels(imageWithBlackBackgound);
      
      // Create an image with transparent pixels where model isn't drawn
      for (int i = 0; i < imageWithBlackBackgoundPixels.length; i++) {
        if (imageWithBlackBackgoundPixels [i] != imageWithWhiteBackgoundPixels [i]
            && imageWithBlackBackgoundPixels [i] == 0xFF000000
            && imageWithWhiteBackgoundPixels [i] == 0xFFFFFFFF) {
          imageWithWhiteBackgoundPixels [i] = 0;
        }           
      }
      
      sceneRoot.removeChild(model);
      return new ImageIcon(Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(
          imageWithWhiteBackgound.getWidth(), imageWithWhiteBackgound.getHeight(), 
          imageWithWhiteBackgoundPixels, 0, imageWithWhiteBackgound.getWidth())));
    }

    /**
     * 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);
          }
        }
      } 
    }

    /**
     * Returns the pixels of the given <code>image</code>.
     */
    private int [] getImagePixels(BufferedImage image) {
      if (image.getType() == BufferedImage.TYPE_INT_RGB
          || image.getType() == BufferedImage.TYPE_INT_ARGB) {
        // Use a faster way to get pixels 
        return (int [])image.getRaster().getDataElements(0, 0, image.getWidth(), image.getHeight(), null);
      } else {
        return image.getRGB(0, 0, image.getWidth(), image.getHeight(), null,
            0, image.getWidth());
      }
    }
  }
}
