1 /* 2 * HomeController3D.java 21 juin 07 3 * 4 * Sweet Home 3D, Copyright (c) 2007 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.viewcontroller; 21 22 import java.beans.PropertyChangeEvent; 23 import java.beans.PropertyChangeListener; 24 import java.lang.ref.WeakReference; 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.Collections; 28 import java.util.List; 29 30 import javax.swing.undo.UndoableEditSupport; 31 32 import com.eteks.sweethome3d.model.Camera; 33 import com.eteks.sweethome3d.model.CollectionEvent; 34 import com.eteks.sweethome3d.model.CollectionListener; 35 import com.eteks.sweethome3d.model.Elevatable; 36 import com.eteks.sweethome3d.model.Home; 37 import com.eteks.sweethome3d.model.HomeEnvironment; 38 import com.eteks.sweethome3d.model.HomeFurnitureGroup; 39 import com.eteks.sweethome3d.model.HomePieceOfFurniture; 40 import com.eteks.sweethome3d.model.Label; 41 import com.eteks.sweethome3d.model.Level; 42 import com.eteks.sweethome3d.model.ObserverCamera; 43 import com.eteks.sweethome3d.model.Polyline; 44 import com.eteks.sweethome3d.model.Room; 45 import com.eteks.sweethome3d.model.Selectable; 46 import com.eteks.sweethome3d.model.SelectionEvent; 47 import com.eteks.sweethome3d.model.SelectionListener; 48 import com.eteks.sweethome3d.model.UserPreferences; 49 import com.eteks.sweethome3d.model.Wall; 50 51 /** 52 * A MVC controller for the home 3D view. 53 * @author Emmanuel Puybaret 54 */ 55 public class HomeController3D implements Controller { 56 private final Home home; 57 private final UserPreferences preferences; 58 private final ViewFactory viewFactory; 59 private final ContentManager contentManager; 60 private final UndoableEditSupport undoSupport; 61 private View home3DView; 62 // Possibles states 63 private final CameraControllerState topCameraState; 64 private final CameraControllerState observerCameraState; 65 // Current state 66 private CameraControllerState cameraState; 67 68 /** 69 * Creates the controller of home 3D view. 70 * @param home the home edited by this controller and its view 71 */ HomeController3D(final Home home, UserPreferences preferences, ViewFactory viewFactory, ContentManager contentManager, UndoableEditSupport undoSupport)72 public HomeController3D(final Home home, 73 UserPreferences preferences, 74 ViewFactory viewFactory, 75 ContentManager contentManager, 76 UndoableEditSupport undoSupport) { 77 this.home = home; 78 this.preferences = preferences; 79 this.viewFactory = viewFactory; 80 this.contentManager = contentManager; 81 this.undoSupport = undoSupport; 82 // Initialize states 83 this.topCameraState = new TopCameraState(preferences); 84 this.observerCameraState = new ObserverCameraState(); 85 // Set default state 86 setCameraState(home.getCamera() == home.getTopCamera() 87 ? this.topCameraState 88 : this.observerCameraState); 89 addModelListeners(home); 90 } 91 92 /** 93 * Add listeners to model to update camera position accordingly. 94 */ addModelListeners(final Home home)95 private void addModelListeners(final Home home) { 96 home.addPropertyChangeListener(Home.Property.CAMERA, new PropertyChangeListener() { 97 public void propertyChange(PropertyChangeEvent ev) { 98 setCameraState(home.getCamera() == home.getTopCamera() 99 ? topCameraState 100 : observerCameraState); 101 } 102 }); 103 // Add listeners to adjust observer camera elevation when the elevation of the selected level 104 // or the level selection change 105 final PropertyChangeListener levelElevationChangeListener = new PropertyChangeListener() { 106 public void propertyChange(PropertyChangeEvent ev) { 107 if (Level.Property.ELEVATION.name().equals(ev.getPropertyName()) 108 && home.getEnvironment().isObserverCameraElevationAdjusted()) { 109 home.getObserverCamera().setZ(Math.max(getObserverCameraMinimumElevation(home), 110 home.getObserverCamera().getZ() + (Float)ev.getNewValue() - (Float)ev.getOldValue())); 111 } 112 } 113 }; 114 Level selectedLevel = home.getSelectedLevel(); 115 if (selectedLevel != null) { 116 selectedLevel.addPropertyChangeListener(levelElevationChangeListener); 117 } 118 home.addPropertyChangeListener(Home.Property.SELECTED_LEVEL, new PropertyChangeListener() { 119 public void propertyChange(PropertyChangeEvent ev) { 120 Level oldSelectedLevel = (Level)ev.getOldValue(); 121 Level selectedLevel = home.getSelectedLevel(); 122 if (home.getEnvironment().isObserverCameraElevationAdjusted()) { 123 home.getObserverCamera().setZ(Math.max(getObserverCameraMinimumElevation(home), 124 home.getObserverCamera().getZ() 125 + (selectedLevel == null ? 0 : selectedLevel.getElevation()) 126 - (oldSelectedLevel == null ? 0 : oldSelectedLevel.getElevation()))); 127 } 128 if (oldSelectedLevel != null) { 129 oldSelectedLevel.removePropertyChangeListener(levelElevationChangeListener); 130 } 131 if (selectedLevel != null) { 132 selectedLevel.addPropertyChangeListener(levelElevationChangeListener); 133 } 134 } 135 }); 136 // Add a listener to home to update visible levels according to selected level 137 PropertyChangeListener selectedLevelListener = new PropertyChangeListener() { 138 public void propertyChange(PropertyChangeEvent ev) { 139 List<Level> levels = home.getLevels(); 140 Level selectedLevel = home.getSelectedLevel(); 141 boolean visible = true; 142 for (int i = 0; i < levels.size(); i++) { 143 levels.get(i).setVisible(visible); 144 if (levels.get(i) == selectedLevel 145 && !home.getEnvironment().isAllLevelsVisible()) { 146 visible = false; 147 } 148 } 149 } 150 }; 151 home.addPropertyChangeListener(Home.Property.SELECTED_LEVEL, selectedLevelListener); 152 home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.ALL_LEVELS_VISIBLE, selectedLevelListener); 153 } 154 getObserverCameraMinimumElevation(final Home home)155 private float getObserverCameraMinimumElevation(final Home home) { 156 List<Level> levels = home.getLevels(); 157 float minimumElevation = levels.size() == 0 ? 10 : 10 + levels.get(0).getElevation(); 158 return minimumElevation; 159 } 160 161 /** 162 * Returns the view associated with this controller. 163 */ getView()164 public View getView() { 165 // Create view lazily only once it's needed 166 if (this.home3DView == null) { 167 this.home3DView = this.viewFactory.createView3D(this.home, this.preferences, this); 168 } 169 return this.home3DView; 170 } 171 172 /** 173 * Changes home camera for {@link Home#getTopCamera() top camera}. 174 */ viewFromTop()175 public void viewFromTop() { 176 this.home.setCamera(this.home.getTopCamera()); 177 } 178 179 /** 180 * Changes home camera for {@link Home#getObserverCamera() observer camera}. 181 */ viewFromObserver()182 public void viewFromObserver() { 183 this.home.setCamera(this.home.getObserverCamera()); 184 } 185 186 /** 187 * Stores a clone of the current camera in home under the given <code>name</code>. 188 */ storeCamera(String name)189 public void storeCamera(String name) { 190 Camera camera = (Camera)this.home.getCamera().duplicate(); 191 camera.setName(name); 192 List<Camera> homeStoredCameras = this.home.getStoredCameras(); 193 ArrayList<Camera> storedCameras = new ArrayList<Camera>(homeStoredCameras.size() + 1); 194 storedCameras.addAll(homeStoredCameras); 195 // Don't keep two cameras with the same name or the same location 196 for (int i = storedCameras.size() - 1; i >= 0; i--) { 197 Camera storedCamera = storedCameras.get(i); 198 if (name.equals(storedCamera.getName()) 199 || (camera.getX() == storedCamera.getX() 200 && camera.getY() == storedCamera.getY() 201 && camera.getZ() == storedCamera.getZ() 202 && camera.getPitch() == storedCamera.getPitch() 203 && camera.getYaw() == storedCamera.getYaw() 204 && camera.getFieldOfView() == storedCamera.getFieldOfView() 205 && camera.getTime() == storedCamera.getTime() 206 && camera.getLens() == storedCamera.getLens())) { 207 storedCameras.remove(i); 208 } 209 } 210 storedCameras.add(0, camera); 211 // Ensure home stored cameras don't contain more cameras than allowed 212 while (storedCameras.size() > this.preferences.getStoredCamerasMaxCount()) { 213 storedCameras.remove(storedCameras.size() - 1); 214 } 215 this.home.setStoredCameras(storedCameras); 216 } 217 218 /** 219 * Switches to observer or top camera and move camera to the values as the current camera. 220 */ goToCamera(Camera camera)221 public void goToCamera(Camera camera) { 222 if (camera instanceof ObserverCamera) { 223 viewFromObserver(); 224 } else { 225 viewFromTop(); 226 } 227 this.cameraState.goToCamera(camera); 228 // Reorder cameras 229 ArrayList<Camera> storedCameras = new ArrayList<Camera>(this.home.getStoredCameras()); 230 storedCameras.remove(camera); 231 storedCameras.add(0, camera); 232 this.home.setStoredCameras(storedCameras); 233 } 234 235 /** 236 * Deletes the given list of cameras from the ones stored in home. 237 */ deleteCameras(List<Camera> cameras)238 public void deleteCameras(List<Camera> cameras) { 239 List<Camera> homeStoredCameras = this.home.getStoredCameras(); 240 // Build a list of cameras that will contain only the cameras not in the camera list in parameter 241 ArrayList<Camera> storedCameras = new ArrayList<Camera>(homeStoredCameras.size() - cameras.size()); 242 for (Camera camera : homeStoredCameras) { 243 if (!cameras.contains(camera)) { 244 storedCameras.add(camera); 245 } 246 } 247 this.home.setStoredCameras(storedCameras); 248 } 249 250 /** 251 * Makes all levels visible. 252 */ displayAllLevels()253 public void displayAllLevels() { 254 this.home.getEnvironment().setAllLevelsVisible(true); 255 } 256 257 /** 258 * Makes the selected level and below visible. 259 */ displaySelectedLevel()260 public void displaySelectedLevel() { 261 this.home.getEnvironment().setAllLevelsVisible(false); 262 } 263 264 /** 265 * Controls the edition of 3D attributes. 266 */ modifyAttributes()267 public void modifyAttributes() { 268 new Home3DAttributesController(this.home, this.preferences, 269 this.viewFactory, this.contentManager, this.undoSupport).displayView(getView()); 270 } 271 272 /** 273 * Changes current state of controller. 274 */ setCameraState(CameraControllerState state)275 protected void setCameraState(CameraControllerState state) { 276 if (this.cameraState != null) { 277 this.cameraState.exit(); 278 } 279 this.cameraState = state; 280 this.cameraState.enter(); 281 } 282 283 /** 284 * Moves home camera of <code>delta</code>. 285 * @param delta the value in cm that the camera should move forward 286 * (with a negative delta) or backward (with a positive delta) 287 */ moveCamera(float delta)288 public void moveCamera(float delta) { 289 this.cameraState.moveCamera(delta); 290 } 291 292 /** 293 * Moves home camera sideways of <code>delta</code>. 294 * @param delta the value in cm that the camera should move left 295 * (with a negative delta) or right (with a positive delta) 296 * @since 4.4 297 */ moveCameraSideways(float delta)298 public void moveCameraSideways(float delta) { 299 this.cameraState.moveCameraSideways(delta); 300 } 301 302 /** 303 * Elevates home camera of <code>delta</code>. 304 * @param delta the value in cm that the camera should move down 305 * (with a negative delta) or up (with a positive delta) 306 */ elevateCamera(float delta)307 public void elevateCamera(float delta) { 308 this.cameraState.elevateCamera(delta); 309 } 310 311 /** 312 * Rotates home camera yaw angle of <code>delta</code> radians. 313 * @param delta the value in rad that the camera should turn around yaw axis 314 */ rotateCameraYaw(float delta)315 public void rotateCameraYaw(float delta) { 316 this.cameraState.rotateCameraYaw(delta); 317 } 318 319 /** 320 * Rotates home camera pitch angle of <code>delta</code> radians. 321 * @param delta the value in rad that the camera should turn around pitch axis 322 */ rotateCameraPitch(float delta)323 public void rotateCameraPitch(float delta) { 324 this.cameraState.rotateCameraPitch(delta); 325 } 326 327 /** 328 * Modifies home camera field of view of <code>delta</code>. 329 * @param delta the value in rad that should be added the field of view 330 * to get a narrower view (with a negative delta) or a wider view (with a positive delta) 331 * @since 5.5 332 */ modifyFieldOfView(float delta)333 public void modifyFieldOfView(float delta) { 334 this.cameraState.modifyFieldOfView(delta); 335 } 336 337 /** 338 * Returns the observer camera state. 339 */ getObserverCameraState()340 protected CameraControllerState getObserverCameraState() { 341 return this.observerCameraState; 342 } 343 344 /** 345 * Returns the top camera state. 346 */ getTopCameraState()347 protected CameraControllerState getTopCameraState() { 348 return this.topCameraState; 349 } 350 351 /** 352 * Controller state classes super class. 353 */ 354 protected static abstract class CameraControllerState { enter()355 public void enter() { 356 } 357 exit()358 public void exit() { 359 } 360 moveCamera(float delta)361 public void moveCamera(float delta) { 362 } 363 moveCameraSideways(float delta)364 public void moveCameraSideways(float delta) { 365 } 366 elevateCamera(float delta)367 public void elevateCamera(float delta) { 368 } 369 rotateCameraYaw(float delta)370 public void rotateCameraYaw(float delta) { 371 } 372 rotateCameraPitch(float delta)373 public void rotateCameraPitch(float delta) { 374 } 375 modifyFieldOfView(float delta)376 public void modifyFieldOfView(float delta) { 377 } 378 goToCamera(Camera camera)379 public void goToCamera(Camera camera) { 380 } 381 } 382 383 // CameraControllerState subclasses 384 385 /** 386 * Top camera controller state. 387 */ 388 private class TopCameraState extends CameraControllerState { 389 private final float MIN_WIDTH = 100; 390 private final float MIN_DEPTH = MIN_WIDTH; 391 private final float MIN_HEIGHT = 20; 392 393 private Camera topCamera; 394 private float [] aerialViewBoundsLowerPoint; 395 private float [] aerialViewBoundsUpperPoint; 396 private float minDistanceToAerialViewCenter; 397 private float maxDistanceToAerialViewCenter; 398 private boolean aerialViewCenteredOnSelectionEnabled; 399 private boolean previousSelectionEmpty; 400 private float distanceToCenterWithSelection = -1; 401 402 private PropertyChangeListener objectChangeListener = new PropertyChangeListener() { 403 public void propertyChange(PropertyChangeEvent ev) { 404 updateCameraFromHomeBounds(false, false); 405 } 406 }; 407 private CollectionListener<Level> levelsListener = new CollectionListener<Level>() { 408 public void collectionChanged(CollectionEvent<Level> ev) { 409 if (ev.getType() == CollectionEvent.Type.ADD) { 410 ev.getItem().addPropertyChangeListener(objectChangeListener); 411 } else if (ev.getType() == CollectionEvent.Type.DELETE) { 412 ev.getItem().removePropertyChangeListener(objectChangeListener); 413 } 414 updateCameraFromHomeBounds(false, false); 415 } 416 }; 417 private CollectionListener<Wall> wallsListener = new CollectionListener<Wall>() { 418 public void collectionChanged(CollectionEvent<Wall> ev) { 419 if (ev.getType() == CollectionEvent.Type.ADD) { 420 ev.getItem().addPropertyChangeListener(objectChangeListener); 421 } else if (ev.getType() == CollectionEvent.Type.DELETE) { 422 ev.getItem().removePropertyChangeListener(objectChangeListener); 423 } 424 updateCameraFromHomeBounds(false, false); 425 } 426 }; 427 private CollectionListener<HomePieceOfFurniture> furnitureListener = new CollectionListener<HomePieceOfFurniture>() { 428 public void collectionChanged(CollectionEvent<HomePieceOfFurniture> ev) { 429 if (ev.getType() == CollectionEvent.Type.ADD) { 430 addPropertyChangeListener(ev.getItem(), objectChangeListener); 431 updateCameraFromHomeBounds(home.getFurniture().size() == 1 432 && home.getWalls().isEmpty() 433 && home.getRooms().isEmpty(), false); 434 } else if (ev.getType() == CollectionEvent.Type.DELETE) { 435 removePropertyChangeListener(ev.getItem(), objectChangeListener); 436 updateCameraFromHomeBounds(false, false); 437 } 438 } 439 }; 440 private CollectionListener<Room> roomsListener = new CollectionListener<Room>() { 441 public void collectionChanged(CollectionEvent<Room> ev) { 442 if (ev.getType() == CollectionEvent.Type.ADD) { 443 ev.getItem().addPropertyChangeListener(objectChangeListener); 444 } else if (ev.getType() == CollectionEvent.Type.DELETE) { 445 ev.getItem().removePropertyChangeListener(objectChangeListener); 446 } 447 updateCameraFromHomeBounds(false, false); 448 } 449 }; 450 private CollectionListener<Polyline> polylinesListener = new CollectionListener<Polyline>() { 451 public void collectionChanged(CollectionEvent<Polyline> ev) { 452 if (ev.getType() == CollectionEvent.Type.ADD) { 453 ev.getItem().addPropertyChangeListener(objectChangeListener); 454 } else if (ev.getType() == CollectionEvent.Type.DELETE) { 455 ev.getItem().removePropertyChangeListener(objectChangeListener); 456 } 457 updateCameraFromHomeBounds(false, false); 458 } 459 }; 460 private CollectionListener<Label> labelsListener = new CollectionListener<Label>() { 461 public void collectionChanged(CollectionEvent<Label> ev) { 462 if (ev.getType() == CollectionEvent.Type.ADD) { 463 ev.getItem().addPropertyChangeListener(objectChangeListener); 464 } else if (ev.getType() == CollectionEvent.Type.DELETE) { 465 ev.getItem().removePropertyChangeListener(objectChangeListener); 466 } 467 updateCameraFromHomeBounds(false, false); 468 } 469 }; 470 private SelectionListener selectionListener = new SelectionListener() { 471 public void selectionChanged(SelectionEvent ev) { 472 boolean selectionEmpty = ev.getSelectedItems().isEmpty(); 473 updateCameraFromHomeBounds(false, previousSelectionEmpty && !selectionEmpty); 474 previousSelectionEmpty = selectionEmpty; 475 } 476 }; 477 private UserPreferencesChangeListener userPreferencesChangeListener; 478 TopCameraState(UserPreferences preferences)479 public TopCameraState(UserPreferences preferences) { 480 this.userPreferencesChangeListener = new UserPreferencesChangeListener(this); 481 } 482 addPropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener)483 private void addPropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener) { 484 if (piece instanceof HomeFurnitureGroup) { 485 for (HomePieceOfFurniture child : ((HomeFurnitureGroup)piece).getFurniture()) { 486 addPropertyChangeListener(child, listener); 487 } 488 } else { 489 piece.addPropertyChangeListener(listener); 490 } 491 } 492 removePropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener)493 private void removePropertyChangeListener(HomePieceOfFurniture piece, PropertyChangeListener listener) { 494 if (piece instanceof HomeFurnitureGroup) { 495 for (HomePieceOfFurniture child : ((HomeFurnitureGroup)piece).getFurniture()) { 496 removePropertyChangeListener(child, listener); 497 } 498 } else { 499 piece.removePropertyChangeListener(listener); 500 } 501 } 502 503 @Override enter()504 public void enter() { 505 this.topCamera = home.getCamera(); 506 this.previousSelectionEmpty = home.getSelectedItems().isEmpty(); 507 this.aerialViewCenteredOnSelectionEnabled = preferences.isAerialViewCenteredOnSelectionEnabled(); 508 updateCameraFromHomeBounds(false, false); 509 for (Level level : home.getLevels()) { 510 level.addPropertyChangeListener(this.objectChangeListener); 511 } 512 home.addLevelsListener(this.levelsListener); 513 for (Wall wall : home.getWalls()) { 514 wall.addPropertyChangeListener(this.objectChangeListener); 515 } 516 home.addWallsListener(this.wallsListener); 517 for (HomePieceOfFurniture piece : home.getFurniture()) { 518 addPropertyChangeListener(piece, this.objectChangeListener); 519 } 520 home.addFurnitureListener(this.furnitureListener); 521 for (Room room : home.getRooms()) { 522 room.addPropertyChangeListener(this.objectChangeListener); 523 } 524 home.addRoomsListener(this.roomsListener); 525 for (Polyline polyline : home.getPolylines()) { 526 polyline.addPropertyChangeListener(this.objectChangeListener); 527 } 528 home.addPolylinesListener(this.polylinesListener); 529 for (Label label : home.getLabels()) { 530 label.addPropertyChangeListener(this.objectChangeListener); 531 } 532 home.addLabelsListener(this.labelsListener); 533 home.addSelectionListener(this.selectionListener); 534 preferences.addPropertyChangeListener(UserPreferences.Property.AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED, 535 this.userPreferencesChangeListener); 536 } 537 538 /** 539 * Sets whether aerial view should be centered on selection or not. 540 */ setAerialViewCenteredOnSelectionEnabled(boolean aerialViewCenteredOnSelectionEnabled)541 public void setAerialViewCenteredOnSelectionEnabled(boolean aerialViewCenteredOnSelectionEnabled) { 542 this.aerialViewCenteredOnSelectionEnabled = aerialViewCenteredOnSelectionEnabled; 543 updateCameraFromHomeBounds(false, false); 544 } 545 546 /** 547 * Updates camera location from home bounds. 548 */ updateCameraFromHomeBounds(boolean firstPieceOfFurnitureAddedToEmptyHome, boolean selectionChange)549 private void updateCameraFromHomeBounds(boolean firstPieceOfFurnitureAddedToEmptyHome, boolean selectionChange) { 550 if (this.aerialViewBoundsLowerPoint == null) { 551 updateAerialViewBoundsFromHomeBounds(this.aerialViewCenteredOnSelectionEnabled); 552 } 553 float distanceToCenter; 554 if (selectionChange 555 && preferences.isAerialViewCenteredOnSelectionEnabled() 556 && this.distanceToCenterWithSelection != -1) { 557 distanceToCenter = this.distanceToCenterWithSelection; 558 } else { 559 distanceToCenter = getCameraToAerialViewCenterDistance(); 560 } 561 if (!home.getSelectedItems().isEmpty()) { 562 this.distanceToCenterWithSelection = distanceToCenter; 563 } 564 updateAerialViewBoundsFromHomeBounds(this.aerialViewCenteredOnSelectionEnabled); 565 updateCameraIntervalToAerialViewCenter(); 566 placeCameraAt(distanceToCenter, firstPieceOfFurnitureAddedToEmptyHome); 567 } 568 569 /** 570 * Returns the distance between the current camera location and home bounds center. 571 */ getCameraToAerialViewCenterDistance()572 private float getCameraToAerialViewCenterDistance() { 573 return (float)Math.sqrt(Math.pow((this.aerialViewBoundsLowerPoint [0] + this.aerialViewBoundsUpperPoint [0]) / 2 - this.topCamera.getX(), 2) 574 + Math.pow((this.aerialViewBoundsLowerPoint [1] + this.aerialViewBoundsUpperPoint [1]) / 2 - this.topCamera.getY(), 2) 575 + Math.pow((this.aerialViewBoundsLowerPoint [2] + this.aerialViewBoundsUpperPoint [2]) / 2 - this.topCamera.getZ(), 2)); 576 } 577 578 /** 579 * Sets the bounds that includes walls, furniture and rooms, or only selected items 580 * if <code>centerOnSelection</code> is <code>true</code>. 581 */ updateAerialViewBoundsFromHomeBounds(boolean centerOnSelection)582 private void updateAerialViewBoundsFromHomeBounds(boolean centerOnSelection) { 583 this.aerialViewBoundsLowerPoint = 584 this.aerialViewBoundsUpperPoint = null; 585 List<Selectable> selectedItems = Collections.emptyList(); 586 if (centerOnSelection) { 587 selectedItems = new ArrayList<Selectable>(); 588 for (Selectable item : home.getSelectedItems()) { 589 if (item instanceof Elevatable 590 && isItemAtVisibleLevel((Elevatable)item) 591 && (!(item instanceof HomePieceOfFurniture) 592 || ((HomePieceOfFurniture)item).isVisible()) 593 && (!(item instanceof Label) 594 || ((Label)item).getPitch() != null)) { 595 selectedItems.add(item); 596 } 597 } 598 } 599 boolean selectionEmpty = selectedItems.size() == 0 || !centerOnSelection; 600 601 // Compute plan bounds to include rooms, walls and furniture 602 boolean containsVisibleWalls = false; 603 for (Wall wall : selectionEmpty 604 ? home.getWalls() 605 : Home.getWallsSubList(selectedItems)) { 606 if (isItemAtVisibleLevel(wall)) { 607 containsVisibleWalls = true; 608 609 float wallElevation = wall.getLevel() != null 610 ? wall.getLevel().getElevation() 611 : 0; 612 float minZ = selectionEmpty 613 ? 0 614 : wallElevation; 615 616 Float height = wall.getHeight(); 617 float maxZ; 618 if (height != null) { 619 maxZ = wallElevation + height; 620 } else { 621 maxZ = wallElevation + home.getWallHeight(); 622 } 623 Float heightAtEnd = wall.getHeightAtEnd(); 624 if (heightAtEnd != null) { 625 maxZ = Math.max(maxZ, wallElevation + heightAtEnd); 626 } 627 for (float [] point : wall.getPoints()) { 628 updateAerialViewBounds(point [0], point [1], minZ, maxZ); 629 } 630 } 631 } 632 633 for (HomePieceOfFurniture piece : selectionEmpty 634 ? home.getFurniture() 635 : Home.getFurnitureSubList(selectedItems)) { 636 if (piece.isVisible() && isItemAtVisibleLevel(piece)) { 637 float minZ; 638 float maxZ; 639 if (selectionEmpty) { 640 minZ = Math.max(0, piece.getGroundElevation()); 641 maxZ = Math.max(0, piece.getGroundElevation() + piece.getHeightInPlan()); 642 } else { 643 minZ = piece.getGroundElevation(); 644 maxZ = piece.getGroundElevation() + piece.getHeightInPlan(); 645 } 646 for (float [] point : piece.getPoints()) { 647 updateAerialViewBounds(point [0], point [1], minZ, maxZ); 648 } 649 } 650 } 651 652 for (Room room : selectionEmpty 653 ? home.getRooms() 654 : Home.getRoomsSubList(selectedItems)) { 655 if (isItemAtVisibleLevel(room)) { 656 float minZ = 0; 657 float maxZ = MIN_HEIGHT; 658 Level roomLevel = room.getLevel(); 659 if (roomLevel != null) { 660 minZ = roomLevel.getElevation() - roomLevel.getFloorThickness(); 661 maxZ = roomLevel.getElevation(); 662 if (selectionEmpty) { 663 minZ = Math.max(0, minZ); 664 maxZ = Math.max(MIN_HEIGHT, roomLevel.getElevation()); 665 } 666 } 667 for (float [] point : room.getPoints()) { 668 updateAerialViewBounds(point [0], point [1], minZ, maxZ); 669 } 670 } 671 } 672 673 for (Polyline polyline : selectionEmpty 674 ? home.getPolylines() 675 : Home.getPolylinesSubList(selectedItems)) { 676 if (polyline.isVisibleIn3D() && isItemAtVisibleLevel(polyline)) { 677 float minZ; 678 float maxZ; 679 if (selectionEmpty) { 680 minZ = Math.max(0, polyline.getGroundElevation()); 681 maxZ = Math.max(MIN_HEIGHT, polyline.getGroundElevation()); 682 } else { 683 minZ = 684 maxZ = polyline.getGroundElevation(); 685 } 686 for (float [] point : polyline.getPoints()) { 687 updateAerialViewBounds(point [0], point [1], minZ, maxZ); 688 } 689 } 690 } 691 692 for (Label label : selectionEmpty 693 ? home.getLabels() 694 : Home.getLabelsSubList(selectedItems)) { 695 if (label.getPitch() != null && isItemAtVisibleLevel(label)) { 696 float minZ; 697 float maxZ; 698 if (selectionEmpty) { 699 minZ = Math.max(0, label.getGroundElevation()); 700 maxZ = Math.max(MIN_HEIGHT, label.getGroundElevation()); 701 } else { 702 minZ = 703 maxZ = label.getGroundElevation(); 704 } 705 for (float [] point : label.getPoints()) { 706 updateAerialViewBounds(point [0], point [1], minZ, maxZ); 707 } 708 } 709 } 710 711 if (this.aerialViewBoundsLowerPoint == null) { 712 this.aerialViewBoundsLowerPoint = new float [] {0, 0, 0}; 713 this.aerialViewBoundsUpperPoint = new float [] {MIN_WIDTH, MIN_DEPTH, MIN_HEIGHT}; 714 } else if (containsVisibleWalls && selectionEmpty) { 715 // If home contains walls, ensure bounds are always minimum 1 meter wide centered in middle of 3D view 716 if (MIN_WIDTH > this.aerialViewBoundsUpperPoint [0] - this.aerialViewBoundsLowerPoint [0]) { 717 this.aerialViewBoundsLowerPoint [0] = (this.aerialViewBoundsLowerPoint [0] + this.aerialViewBoundsUpperPoint [0]) / 2 - MIN_WIDTH / 2; 718 this.aerialViewBoundsUpperPoint [0] = this.aerialViewBoundsLowerPoint [0] + MIN_WIDTH; 719 } 720 if (MIN_DEPTH > this.aerialViewBoundsUpperPoint [1] - this.aerialViewBoundsLowerPoint [1]) { 721 this.aerialViewBoundsLowerPoint [1] = (this.aerialViewBoundsLowerPoint [1] + this.aerialViewBoundsUpperPoint [1]) / 2 - MIN_DEPTH / 2; 722 this.aerialViewBoundsUpperPoint [1] = this.aerialViewBoundsLowerPoint [1] + MIN_DEPTH; 723 } 724 if (MIN_HEIGHT > this.aerialViewBoundsUpperPoint [2] - this.aerialViewBoundsLowerPoint [2]) { 725 this.aerialViewBoundsLowerPoint [2] = (this.aerialViewBoundsLowerPoint [2] + this.aerialViewBoundsUpperPoint [2]) / 2 - MIN_HEIGHT / 2; 726 this.aerialViewBoundsUpperPoint [2] = this.aerialViewBoundsLowerPoint [2] + MIN_HEIGHT; 727 } 728 } 729 } 730 731 /** 732 * Adds the point at the given coordinates to aerial view bounds. 733 */ updateAerialViewBounds(float x, float y, float minZ, float maxZ)734 private void updateAerialViewBounds(float x, float y, float minZ, float maxZ) { 735 if (this.aerialViewBoundsLowerPoint == null) { 736 this.aerialViewBoundsLowerPoint = new float [] {x, y, minZ}; 737 this.aerialViewBoundsUpperPoint = new float [] {x, y, maxZ}; 738 } else { 739 this.aerialViewBoundsLowerPoint [0] = Math.min(this.aerialViewBoundsLowerPoint [0], x); 740 this.aerialViewBoundsUpperPoint [0] = Math.max(this.aerialViewBoundsUpperPoint [0], x); 741 this.aerialViewBoundsLowerPoint [1] = Math.min(this.aerialViewBoundsLowerPoint [1], y); 742 this.aerialViewBoundsUpperPoint [1] = Math.max(this.aerialViewBoundsUpperPoint [1], y); 743 this.aerialViewBoundsLowerPoint [2] = Math.min(this.aerialViewBoundsLowerPoint [2], minZ); 744 this.aerialViewBoundsUpperPoint [2] = Math.max(this.aerialViewBoundsUpperPoint [2], maxZ); 745 } 746 } 747 748 /** 749 * Returns <code>true</code> if the given <code>item</code> is at a visible level. 750 */ isItemAtVisibleLevel(Elevatable item)751 private boolean isItemAtVisibleLevel(Elevatable item) { 752 return item.getLevel() == null || item.getLevel().isViewableAndVisible(); 753 } 754 755 /** 756 * Updates the minimum and maximum distances of the camera to the center of the aerial view. 757 */ updateCameraIntervalToAerialViewCenter()758 private void updateCameraIntervalToAerialViewCenter() { 759 float homeBoundsWidth = this.aerialViewBoundsUpperPoint [0] - this.aerialViewBoundsLowerPoint [0]; 760 float homeBoundsDepth = this.aerialViewBoundsUpperPoint [1] - this.aerialViewBoundsLowerPoint [1]; 761 float homeBoundsHeight = this.aerialViewBoundsUpperPoint [2] - this.aerialViewBoundsLowerPoint [2]; 762 float halfDiagonal = (float)Math.sqrt(homeBoundsWidth * homeBoundsWidth 763 + homeBoundsDepth * homeBoundsDepth 764 + homeBoundsHeight * homeBoundsHeight) / 2; 765 this.minDistanceToAerialViewCenter = halfDiagonal * 1.05f; 766 this.maxDistanceToAerialViewCenter = Math.max(5 * this.minDistanceToAerialViewCenter, 5000); 767 } 768 769 @Override moveCamera(float delta)770 public void moveCamera(float delta) { 771 // Use a 5 times bigger delta for top camera move 772 delta *= 5; 773 float newDistanceToCenter = getCameraToAerialViewCenterDistance() - delta; 774 placeCameraAt(newDistanceToCenter, false); 775 } 776 placeCameraAt(float distanceToCenter, boolean firstPieceOfFurnitureAddedToEmptyHome)777 public void placeCameraAt(float distanceToCenter, boolean firstPieceOfFurnitureAddedToEmptyHome) { 778 // Check camera is always outside the sphere centered in home center and with a radius equal to minimum distance 779 distanceToCenter = Math.max(distanceToCenter, this.minDistanceToAerialViewCenter); 780 // Check camera isn't too far 781 distanceToCenter = Math.min(distanceToCenter, this.maxDistanceToAerialViewCenter); 782 if (firstPieceOfFurnitureAddedToEmptyHome) { 783 // Get closer to the first piece of furniture added to an empty home when that is small 784 distanceToCenter = Math.min(distanceToCenter, 3 * this.minDistanceToAerialViewCenter); 785 } 786 double distanceToCenterAtGroundLevel = distanceToCenter * Math.cos(this.topCamera.getPitch()); 787 this.topCamera.setX((this.aerialViewBoundsLowerPoint [0] + this.aerialViewBoundsUpperPoint [0]) / 2 788 + (float)(Math.sin(this.topCamera.getYaw()) * distanceToCenterAtGroundLevel)); 789 this.topCamera.setY((this.aerialViewBoundsLowerPoint [1] + this.aerialViewBoundsUpperPoint [1]) / 2 790 - (float)(Math.cos(this.topCamera.getYaw()) * distanceToCenterAtGroundLevel)); 791 this.topCamera.setZ((this.aerialViewBoundsLowerPoint [2] + this.aerialViewBoundsUpperPoint [2]) / 2 792 + (float)Math.sin(this.topCamera.getPitch()) * distanceToCenter); 793 } 794 795 @Override rotateCameraYaw(float delta)796 public void rotateCameraYaw(float delta) { 797 float newYaw = this.topCamera.getYaw() + delta; 798 double distanceToCenterAtGroundLevel = getCameraToAerialViewCenterDistance() * Math.cos(this.topCamera.getPitch()); 799 // Change camera yaw and location so user turns around home 800 this.topCamera.setYaw(newYaw); 801 this.topCamera.setX((this.aerialViewBoundsLowerPoint [0] + this.aerialViewBoundsUpperPoint [0]) / 2 802 + (float)(Math.sin(newYaw) * distanceToCenterAtGroundLevel)); 803 this.topCamera.setY((this.aerialViewBoundsLowerPoint [1] + this.aerialViewBoundsUpperPoint [1]) / 2 804 - (float)(Math.cos(newYaw) * distanceToCenterAtGroundLevel)); 805 } 806 807 @Override rotateCameraPitch(float delta)808 public void rotateCameraPitch(float delta) { 809 float newPitch = this.topCamera.getPitch() + delta; 810 // Check new pitch is between 0 and PI / 2 811 newPitch = Math.max(newPitch, (float)0); 812 newPitch = Math.min(newPitch, (float)Math.PI / 2); 813 // Compute new z to keep the same distance to view center 814 double distanceToCenter = getCameraToAerialViewCenterDistance(); 815 double distanceToCenterAtGroundLevel = distanceToCenter * Math.cos(newPitch); 816 // Change camera pitch 817 this.topCamera.setPitch(newPitch); 818 this.topCamera.setX((this.aerialViewBoundsLowerPoint [0] + this.aerialViewBoundsUpperPoint [0]) / 2 819 + (float)(Math.sin(this.topCamera.getYaw()) * distanceToCenterAtGroundLevel)); 820 this.topCamera.setY((this.aerialViewBoundsLowerPoint [1] + this.aerialViewBoundsUpperPoint [1]) / 2 821 - (float)(Math.cos(this.topCamera.getYaw()) * distanceToCenterAtGroundLevel)); 822 this.topCamera.setZ((this.aerialViewBoundsLowerPoint [2] + this.aerialViewBoundsUpperPoint [2]) / 2 823 + (float)(distanceToCenter * Math.sin(newPitch))); 824 } 825 826 @Override goToCamera(Camera camera)827 public void goToCamera(Camera camera) { 828 this.topCamera.setCamera(camera); 829 this.topCamera.setTime(camera.getTime()); 830 this.topCamera.setLens(camera.getLens()); 831 updateCameraFromHomeBounds(false, false); 832 } 833 834 @Override exit()835 public void exit() { 836 this.topCamera = null; 837 for (Wall wall : home.getWalls()) { 838 wall.removePropertyChangeListener(this.objectChangeListener); 839 } 840 home.removeWallsListener(this.wallsListener); 841 for (HomePieceOfFurniture piece : home.getFurniture()) { 842 removePropertyChangeListener(piece, this.objectChangeListener); 843 } 844 home.removeFurnitureListener(this.furnitureListener); 845 for (Room room : home.getRooms()) { 846 room.removePropertyChangeListener(this.objectChangeListener); 847 } 848 home.removeRoomsListener(this.roomsListener); 849 for (Polyline polyline : home.getPolylines()) { 850 polyline.removePropertyChangeListener(this.objectChangeListener); 851 } 852 home.removePolylinesListener(this.polylinesListener); 853 for (Label label : home.getLabels()) { 854 label.removePropertyChangeListener(this.objectChangeListener); 855 } 856 home.removeLabelsListener(this.labelsListener); 857 for (Level level : home.getLevels()) { 858 level.removePropertyChangeListener(this.objectChangeListener); 859 } 860 home.removeLevelsListener(this.levelsListener); 861 home.removeSelectionListener(this.selectionListener); 862 preferences.removePropertyChangeListener(UserPreferences.Property.AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED, 863 this.userPreferencesChangeListener); 864 } 865 } 866 867 /** 868 * Preferences property listener bound to top camera state with a weak reference to avoid 869 * strong link between user preferences and top camera state. 870 */ 871 private static class UserPreferencesChangeListener implements PropertyChangeListener { 872 private WeakReference<TopCameraState> topCameraState; 873 UserPreferencesChangeListener(TopCameraState topCameraState)874 public UserPreferencesChangeListener(TopCameraState topCameraState) { 875 this.topCameraState = new WeakReference<TopCameraState>(topCameraState); 876 } 877 propertyChange(PropertyChangeEvent ev)878 public void propertyChange(PropertyChangeEvent ev) { 879 // If top camera state was garbage collected, remove this listener from preferences 880 TopCameraState topCameraState = this.topCameraState.get(); 881 UserPreferences preferences = (UserPreferences)ev.getSource(); 882 if (topCameraState == null) { 883 preferences.removePropertyChangeListener(UserPreferences.Property.valueOf(ev.getPropertyName()), this); 884 } else { 885 topCameraState.setAerialViewCenteredOnSelectionEnabled(preferences.isAerialViewCenteredOnSelectionEnabled()); 886 } 887 } 888 } 889 890 /** 891 * Observer camera controller state. 892 */ 893 private class ObserverCameraState extends CameraControllerState { 894 private ObserverCamera observerCamera; 895 private PropertyChangeListener levelElevationChangeListener = new PropertyChangeListener() { 896 public void propertyChange(PropertyChangeEvent ev) { 897 if (Level.Property.ELEVATION.name().equals(ev.getPropertyName())) { 898 updateCameraMinimumElevation(); 899 } 900 } 901 }; 902 private CollectionListener<Level> levelsListener = new CollectionListener<Level>() { 903 public void collectionChanged(CollectionEvent<Level> ev) { 904 if (ev.getType() == CollectionEvent.Type.ADD) { 905 ev.getItem().addPropertyChangeListener(levelElevationChangeListener); 906 } else if (ev.getType() == CollectionEvent.Type.DELETE) { 907 ev.getItem().removePropertyChangeListener(levelElevationChangeListener); 908 } 909 updateCameraMinimumElevation(); 910 } 911 }; 912 913 @Override enter()914 public void enter() { 915 this.observerCamera = (ObserverCamera)home.getCamera(); 916 for (Level level : home.getLevels()) { 917 level.addPropertyChangeListener(this.levelElevationChangeListener); 918 } 919 home.addLevelsListener(this.levelsListener); 920 if (preferences.isObserverCameraSelectedAtChange()) { 921 // Select observer camera for user feedback 922 home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); 923 } 924 } 925 926 @Override moveCamera(float delta)927 public void moveCamera(float delta) { 928 this.observerCamera.setX(this.observerCamera.getX() - (float)Math.sin(this.observerCamera.getYaw()) * delta); 929 this.observerCamera.setY(this.observerCamera.getY() + (float)Math.cos(this.observerCamera.getYaw()) * delta); 930 if (preferences.isObserverCameraSelectedAtChange()) { 931 // Select observer camera for user feedback 932 home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); 933 } 934 } 935 936 @Override moveCameraSideways(float delta)937 public void moveCameraSideways(float delta) { 938 this.observerCamera.setX(this.observerCamera.getX() - (float)Math.cos(this.observerCamera.getYaw()) * delta); 939 this.observerCamera.setY(this.observerCamera.getY() - (float)Math.sin(this.observerCamera.getYaw()) * delta); 940 if (preferences.isObserverCameraSelectedAtChange()) { 941 // Select observer camera for user feedback 942 home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); 943 } 944 } 945 946 @Override elevateCamera(float delta)947 public void elevateCamera(float delta) { 948 float newElevation = this.observerCamera.getZ() + delta; 949 newElevation = Math.min(Math.max(newElevation, getMinimumElevation()), preferences.getLengthUnit().getMaximumElevation()); 950 this.observerCamera.setZ(newElevation); 951 if (preferences.isObserverCameraSelectedAtChange()) { 952 // Select observer camera for user feedback 953 home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); 954 } 955 } 956 updateCameraMinimumElevation()957 private void updateCameraMinimumElevation() { 958 observerCamera.setZ(Math.max(observerCamera.getZ(), getMinimumElevation())); 959 } 960 getMinimumElevation()961 public float getMinimumElevation() { 962 List<Level> levels = home.getLevels(); 963 if (levels.size() > 0) { 964 return 10 + levels.get(0).getElevation(); 965 } else { 966 return 10; 967 } 968 } 969 970 @Override rotateCameraYaw(float delta)971 public void rotateCameraYaw(float delta) { 972 this.observerCamera.setYaw(this.observerCamera.getYaw() + delta); 973 // Select observer camera for user feedback 974 if (preferences.isObserverCameraSelectedAtChange()) { 975 // Select observer camera for user feedback 976 home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); 977 } 978 } 979 980 @Override rotateCameraPitch(float delta)981 public void rotateCameraPitch(float delta) { 982 float newPitch = this.observerCamera.getPitch() + delta; 983 // Check new angle is between -90� and 90� 984 newPitch = Math.min(Math.max(-(float)Math.PI / 2, newPitch), (float)Math.PI / 2);; 985 this.observerCamera.setPitch(newPitch); 986 if (preferences.isObserverCameraSelectedAtChange()) { 987 // Select observer camera for user feedback 988 home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); 989 } 990 } 991 992 @Override modifyFieldOfView(float delta)993 public void modifyFieldOfView(float delta) { 994 float newFieldOfView = this.observerCamera.getFieldOfView() + delta; 995 // Check new angle is between 2� and 120� 996 newFieldOfView = (float)Math.min(Math.max(Math.toRadians(2), newFieldOfView), Math.toRadians(120)); 997 this.observerCamera.setFieldOfView(newFieldOfView); 998 if (preferences.isObserverCameraSelectedAtChange()) { 999 // Select observer camera for user feedback 1000 home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); 1001 } 1002 } 1003 1004 @Override goToCamera(Camera camera)1005 public void goToCamera(Camera camera) { 1006 this.observerCamera.setCamera(camera); 1007 this.observerCamera.setTime(camera.getTime()); 1008 this.observerCamera.setLens(camera.getLens()); 1009 } 1010 1011 @Override exit()1012 public void exit() { 1013 // Remove observer camera from selection 1014 List<Selectable> selectedItems = home.getSelectedItems(); 1015 if (selectedItems.contains(this.observerCamera)) { 1016 selectedItems = new ArrayList<Selectable>(selectedItems); 1017 selectedItems.remove(this.observerCamera); 1018 home.setSelectedItems(selectedItems); 1019 } 1020 for (Level level : home.getLevels()) { 1021 level.removePropertyChangeListener(this.levelElevationChangeListener); 1022 } 1023 home.removeLevelsListener(this.levelsListener); 1024 this.observerCamera = null; 1025 } 1026 } 1027 } 1028