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