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