/* * HomeController3D.java 21 juin 07 * * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.eteks.sweethome3d.viewcontroller; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import javax.swing.undo.UndoableEditSupport; import com.eteks.sweethome3d.model.Camera; import com.eteks.sweethome3d.model.CollectionEvent; import com.eteks.sweethome3d.model.CollectionListener; import com.eteks.sweethome3d.model.Elevatable; import com.eteks.sweethome3d.model.Home; import com.eteks.sweethome3d.model.HomeEnvironment; import com.eteks.sweethome3d.model.HomePieceOfFurniture; import com.eteks.sweethome3d.model.Level; import com.eteks.sweethome3d.model.ObserverCamera; import com.eteks.sweethome3d.model.Room; import com.eteks.sweethome3d.model.Selectable; import com.eteks.sweethome3d.model.SelectionEvent; import com.eteks.sweethome3d.model.SelectionListener; import com.eteks.sweethome3d.model.UserPreferences; import com.eteks.sweethome3d.model.Wall; /** * A MVC controller for the home 3D view. * @author Emmanuel Puybaret */ public class HomeController3D implements Controller { private final Home home; private final UserPreferences preferences; private final ViewFactory viewFactory; private final ContentManager contentManager; private final UndoableEditSupport undoSupport; private View home3DView; // Possibles states private final CameraControllerState topCameraState; private final CameraControllerState observerCameraState; // Current state private CameraControllerState cameraState; /** * Creates the controller of home 3D view. * @param home the home edited by this controller and its view */ public HomeController3D(final Home home, UserPreferences preferences, ViewFactory viewFactory, ContentManager contentManager, UndoableEditSupport undoSupport) { this.home = home; this.preferences = preferences; this.viewFactory = viewFactory; this.contentManager = contentManager; this.undoSupport = undoSupport; // Initialize states this.topCameraState = new TopCameraState(preferences); this.observerCameraState = new ObserverCameraState(); // Set default state setCameraState(home.getCamera() == home.getTopCamera() ? this.topCameraState : this.observerCameraState); addModelListeners(home); } /** * Add listeners to model to update camera position accordingly. */ private void addModelListeners(final Home home) { home.addPropertyChangeListener(Home.Property.CAMERA, new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent ev) { setCameraState(home.getCamera() == home.getTopCamera() ? topCameraState : observerCameraState); } }); // Add listeners to adjust observer camera elevation when the elevation of the selected level // or the level selection change final PropertyChangeListener levelElevationChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent ev) { if (Level.Property.ELEVATION.name().equals(ev.getPropertyName()) && home.getEnvironment().isObserverCameraElevationAdjusted()) { home.getObserverCamera().setZ(Math.max(getObserverCameraMinimumElevation(home), home.getObserverCamera().getZ() + (Float)ev.getNewValue() - (Float)ev.getOldValue())); } } }; Level selectedLevel = home.getSelectedLevel(); if (selectedLevel != null) { selectedLevel.addPropertyChangeListener(levelElevationChangeListener); } this.home.addPropertyChangeListener(Home.Property.SELECTED_LEVEL, new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent ev) { Level oldSelectedLevel = (Level)ev.getOldValue(); Level selectedLevel = home.getSelectedLevel(); if (home.getEnvironment().isObserverCameraElevationAdjusted()) { home.getObserverCamera().setZ(Math.max(getObserverCameraMinimumElevation(home), home.getObserverCamera().getZ() + (selectedLevel == null ? 0 : selectedLevel.getElevation()) - (oldSelectedLevel == null ? 0 : oldSelectedLevel.getElevation()))); } if (oldSelectedLevel != null) { oldSelectedLevel.removePropertyChangeListener(levelElevationChangeListener); } if (selectedLevel != null) { selectedLevel.addPropertyChangeListener(levelElevationChangeListener); } } }); // Add a listener to home to update visible levels according to selected level PropertyChangeListener selectedLevelListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent ev) { List<Level> levels = home.getLevels(); Level selectedLevel = home.getSelectedLevel(); boolean visible = true; for (int i = 0; i < levels.size(); i++) { levels.get(i).setVisible(visible); if (levels.get(i) == selectedLevel && !home.getEnvironment().isAllLevelsVisible()) { visible = false; } } } }; this.home.addPropertyChangeListener(Home.Property.SELECTED_LEVEL, selectedLevelListener); this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.ALL_LEVELS_VISIBLE, selectedLevelListener); } private float getObserverCameraMinimumElevation(final Home home) { List<Level> levels = home.getLevels(); float minimumElevation = levels.size() == 0 ? 10 : 10 + levels.get(0).getElevation(); return minimumElevation; } /** * Returns the view associated with this controller. */ public View getView() { // Create view lazily only once it's needed if (this.home3DView == null) { this.home3DView = this.viewFactory.createView3D(this.home, this.preferences, this); } return this.home3DView; } /** * Changes home camera for {@link Home#getTopCamera() top camera}. */ public void viewFromTop() { this.home.setCamera(this.home.getTopCamera()); } /** * Changes home camera for {@link Home#getObserverCamera() observer camera}. */ public void viewFromObserver() { this.home.setCamera(this.home.getObserverCamera()); } /** * Stores a clone of the current camera in home under the given <code>name</code>. */ public void storeCamera(String name) { Camera camera = this.home.getCamera().clone(); camera.setName(name); List<Camera> homeStoredCameras = this.home.getStoredCameras(); ArrayList<Camera> storedCameras = new ArrayList<Camera>(homeStoredCameras.size() + 1); storedCameras.addAll(homeStoredCameras); // Don't keep two cameras with the same name or the same location for (Iterator<Camera> it = storedCameras.iterator(); it.hasNext(); ) { Camera storedCamera = it.next(); if (name.equals(storedCamera.getName()) || (camera.getX() == storedCamera.getX() && camera.getY() == storedCamera.getY() && camera.getZ() == storedCamera.getZ() && camera.getPitch() == storedCamera.getPitch() && camera.getYaw() == storedCamera.getYaw() && camera.getFieldOfView() == storedCamera.getFieldOfView() && camera.getTime() == storedCamera.getTime() && camera.getLens() == storedCamera.getLens())) { it.remove(); } } storedCameras.add(0, camera); // Ensure home stored cameras don't contain more than 20 cameras while (storedCameras.size() > 20) { storedCameras.remove(storedCameras.size() - 1); } this.home.setStoredCameras(storedCameras); } /** * Switches to observer or top camera and move camera to the values as the current camera. */ public void goToCamera(Camera camera) { if (camera instanceof ObserverCamera) { viewFromObserver(); } else { viewFromTop(); } this.cameraState.goToCamera(camera); // Reorder cameras ArrayList<Camera> storedCameras = new ArrayList<Camera>(this.home.getStoredCameras()); storedCameras.remove(camera); storedCameras.add(0, camera); this.home.setStoredCameras(storedCameras); } /** * Deletes the given list of cameras from the ones stored in home. */ public void deleteCameras(List<Camera> cameras) { List<Camera> homeStoredCameras = this.home.getStoredCameras(); // Build a list of cameras that will contain only the cameras not in the camera list in parameter ArrayList<Camera> storedCameras = new ArrayList<Camera>(homeStoredCameras.size() - cameras.size()); for (Camera camera : homeStoredCameras) { if (!cameras.contains(camera)) { storedCameras.add(camera); } } this.home.setStoredCameras(storedCameras); } /** * Makes all levels visible. */ public void displayAllLevels() { this.home.getEnvironment().setAllLevelsVisible(true); } /** * Makes the selected level and below visible. */ public void displaySelectedLevel() { this.home.getEnvironment().setAllLevelsVisible(false); } /** * Controls the edition of 3D attributes. */ public void modifyAttributes() { new Home3DAttributesController(this.home, this.preferences, this.viewFactory, this.contentManager, this.undoSupport).displayView(getView()); } /** * Changes current state of controller. */ protected void setCameraState(CameraControllerState state) { if (this.cameraState != null) { this.cameraState.exit(); } this.cameraState = state; this.cameraState.enter(); } /** * Moves home camera of <code>delta</code>. */ public void moveCamera(float delta) { this.cameraState.moveCamera(delta); } /** * Elevates home camera of <code>delta</code>. */ public void elevateCamera(float delta) { this.cameraState.elevateCamera(delta); } /** * Rotates home camera yaw angle of <code>delta</code> radians. */ public void rotateCameraYaw(float delta) { this.cameraState.rotateCameraYaw(delta); } /** * Rotates home camera pitch angle of <code>delta</code> radians. */ public void rotateCameraPitch(float delta) { this.cameraState.rotateCameraPitch(delta); } /** * Returns the observer camera state. */ protected CameraControllerState getObserverCameraState() { return this.observerCameraState; } /** * Returns the top camera state. */ protected CameraControllerState getTopCameraState() { return this.topCameraState; } /** * Controller state classes super class. */ protected static abstract class CameraControllerState { public void enter() { } public void exit() { } public void moveCamera(float delta) { } public void elevateCamera(float delta) { } public void rotateCameraYaw(float delta) { } public void rotateCameraPitch(float delta) { } public void goToCamera(Camera camera) { } } // CameraControllerState subclasses /** * Top camera controller state. */ private class TopCameraState extends CameraControllerState { private final float MIN_WIDTH = 1000; private final float MIN_DEPTH = 1000; private final float MIN_HEIGHT = 20; private Camera topCamera; private float [] aerialViewBoundsLowerPoint; private float [] aerialViewBoundsUpperPoint; private float minDistanceToAerialViewCenter; private float maxDistanceToAerialViewCenter; private boolean aerialViewCenteredOnSelectionEnabled; private PropertyChangeListener objectChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent ev) { updateCameraFromHomeBounds(false); } }; private CollectionListener<Level> levelsListener = new CollectionListener<Level>() { public void collectionChanged(CollectionEvent<Level> ev) { if (ev.getType() == CollectionEvent.Type.ADD) { ev.getItem().addPropertyChangeListener(objectChangeListener); } else if (ev.getType() == CollectionEvent.Type.DELETE) { ev.getItem().removePropertyChangeListener(objectChangeListener); } updateCameraFromHomeBounds(false); } }; private CollectionListener<Wall> wallsListener = new CollectionListener<Wall>() { public void collectionChanged(CollectionEvent<Wall> ev) { if (ev.getType() == CollectionEvent.Type.ADD) { ev.getItem().addPropertyChangeListener(objectChangeListener); } else if (ev.getType() == CollectionEvent.Type.DELETE) { ev.getItem().removePropertyChangeListener(objectChangeListener); } updateCameraFromHomeBounds(false); } }; private CollectionListener<HomePieceOfFurniture> furnitureListener = new CollectionListener<HomePieceOfFurniture>() { public void collectionChanged(CollectionEvent<HomePieceOfFurniture> ev) { if (ev.getType() == CollectionEvent.Type.ADD) { ev.getItem().addPropertyChangeListener(objectChangeListener); updateCameraFromHomeBounds(home.getFurniture().size() == 1 && home.getWalls().isEmpty() && home.getRooms().isEmpty()); } else if (ev.getType() == CollectionEvent.Type.DELETE) { ev.getItem().removePropertyChangeListener(objectChangeListener); updateCameraFromHomeBounds(false); } } }; private CollectionListener<Room> roomsListener = new CollectionListener<Room>() { public void collectionChanged(CollectionEvent<Room> ev) { if (ev.getType() == CollectionEvent.Type.ADD) { ev.getItem().addPropertyChangeListener(objectChangeListener); } else if (ev.getType() == CollectionEvent.Type.DELETE) { ev.getItem().removePropertyChangeListener(objectChangeListener); } updateCameraFromHomeBounds(false); } }; private SelectionListener selectionListener = new SelectionListener() { public void selectionChanged(SelectionEvent ev) { updateCameraFromHomeBounds(false); } }; public TopCameraState(UserPreferences preferences) { this.aerialViewCenteredOnSelectionEnabled = preferences.isAerialViewCenteredOnSelectionEnabled(); preferences.addPropertyChangeListener(UserPreferences.Property.AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED, new UserPreferencesChangeListener(this)); } @Override public void enter() { this.topCamera = home.getCamera(); updateCameraFromHomeBounds(false); for (Level level : home.getLevels()) { level.addPropertyChangeListener(this.objectChangeListener); } home.addLevelsListener(this.levelsListener); for (Wall wall : home.getWalls()) { wall.addPropertyChangeListener(this.objectChangeListener); } home.addWallsListener(this.wallsListener); for (HomePieceOfFurniture piece : home.getFurniture()) { piece.addPropertyChangeListener(this.objectChangeListener); } home.addFurnitureListener(this.furnitureListener); for (Room room : home.getRooms()) { room.addPropertyChangeListener(this.objectChangeListener); } home.addRoomsListener(this.roomsListener); home.addSelectionListener(this.selectionListener); } /** * Sets whether aerial view should be centered on selection or not. */ public void setAerialViewCenteredOnSelectionEnabled(boolean aerialViewCenteredOnSelectionEnabled) { this.aerialViewCenteredOnSelectionEnabled = aerialViewCenteredOnSelectionEnabled; updateCameraFromHomeBounds(false); } /** * Updates camera location from home bounds. */ private void updateCameraFromHomeBounds(boolean firstPieceOfFurnitureAddedToEmptyHome) { if (this.aerialViewBoundsLowerPoint == null) { updateAerialViewBounds(this.aerialViewCenteredOnSelectionEnabled); } float distanceToCenter = getCameraToAerialViewCenterDistance(); updateAerialViewBounds(this.aerialViewCenteredOnSelectionEnabled); updateCameraIntervalToAerialViewCenter(); placeCameraAt(distanceToCenter, firstPieceOfFurnitureAddedToEmptyHome); } /** * Returns the distance between the current camera location and home bounds center. */ private float getCameraToAerialViewCenterDistance() { return (float)Math.sqrt(Math.pow((this.aerialViewBoundsLowerPoint [0] + this.aerialViewBoundsUpperPoint [0]) / 2 - this.topCamera.getX(), 2) + Math.pow((this.aerialViewBoundsLowerPoint [1] + this.aerialViewBoundsUpperPoint [1]) / 2 - this.topCamera.getY(), 2) + Math.pow((this.aerialViewBoundsLowerPoint [2] + this.aerialViewBoundsUpperPoint [2]) / 2 - this.topCamera.getZ(), 2)); } /** * Sets the bounds that includes walls, furniture and rooms, or only selected items * if <code>centerOnSelection</code> is <code>true</code>. */ private void updateAerialViewBounds(boolean centerOnSelection) { this.aerialViewBoundsLowerPoint = this.aerialViewBoundsUpperPoint = null; List<Selectable> selectedItems = Collections.emptyList(); if (centerOnSelection) { selectedItems = new ArrayList<Selectable>(); for (Selectable item : home.getSelectedItems()) { if (item instanceof Elevatable && isItemAtVisibleLevel((Elevatable)item) && (!(item instanceof HomePieceOfFurniture) || ((HomePieceOfFurniture)item).isVisible())) { selectedItems.add(item); } } } boolean selectionEmpty = selectedItems.size() == 0 || !centerOnSelection; // Compute plan bounds to include rooms, walls and furniture boolean containsVisibleWalls = false; for (Wall wall : selectionEmpty ? home.getWalls() : Home.getWallsSubList(selectedItems)) { if (isItemAtVisibleLevel(wall)) { containsVisibleWalls = true; float wallElevation = wall.getLevel() != null ? wall.getLevel().getElevation() : 0; float minZ = selectionEmpty ? 0 : wallElevation; Float height = wall.getHeight(); float maxZ; if (height != null) { maxZ = wallElevation + height; } else { maxZ = wallElevation + home.getWallHeight(); } Float heightAtEnd = wall.getHeightAtEnd(); if (heightAtEnd != null) { maxZ = Math.max(maxZ, wallElevation + heightAtEnd); } for (float [] point : wall.getPoints()) { updateAerialViewBounds(point [0], point [1], minZ, maxZ); } } } for (HomePieceOfFurniture piece : selectionEmpty ? home.getFurniture() : Home.getFurnitureSubList(selectedItems)) { if (piece.isVisible() && isItemAtVisibleLevel(piece)) { float minZ; float maxZ; if (selectionEmpty) { minZ = Math.max(0, piece.getGroundElevation()); maxZ = Math.max(0, piece.getGroundElevation() + piece.getHeight()); } else { minZ = piece.getGroundElevation(); maxZ = piece.getGroundElevation() + piece.getHeight(); } for (float [] point : piece.getPoints()) { updateAerialViewBounds(point [0], point [1], minZ, maxZ); } } } for (Room room : selectionEmpty ? home.getRooms() : Home.getRoomsSubList(selectedItems)) { if (isItemAtVisibleLevel(room)) { float minZ = 0; float maxZ = MIN_HEIGHT; Level roomLevel = room.getLevel(); if (roomLevel != null) { minZ = roomLevel.getElevation() - roomLevel.getFloorThickness(); maxZ = roomLevel.getElevation(); if (selectionEmpty) { minZ = Math.max(0, minZ); maxZ = Math.max(MIN_HEIGHT, roomLevel.getElevation()); } } for (float [] point : room.getPoints()) { updateAerialViewBounds(point [0], point [1], minZ, maxZ); } } } if (this.aerialViewBoundsLowerPoint == null) { this.aerialViewBoundsLowerPoint = new float [] {0, 0, 0}; this.aerialViewBoundsUpperPoint = new float [] {MIN_WIDTH, MIN_DEPTH, MIN_HEIGHT}; } else if (containsVisibleWalls && selectionEmpty) { // If home contains walls, ensure bounds are always minimum 10 meters wide centered in middle of 3D view if (MIN_WIDTH > this.aerialViewBoundsUpperPoint [0] - this.aerialViewBoundsLowerPoint [0]) { this.aerialViewBoundsLowerPoint [0] = (this.aerialViewBoundsLowerPoint [0] + this.aerialViewBoundsUpperPoint [0]) / 2 - MIN_WIDTH / 2; this.aerialViewBoundsUpperPoint [0] = this.aerialViewBoundsLowerPoint [0] + MIN_WIDTH; } if (MIN_DEPTH > this.aerialViewBoundsUpperPoint [1] - this.aerialViewBoundsLowerPoint [1]) { this.aerialViewBoundsLowerPoint [1] = (this.aerialViewBoundsLowerPoint [1] + this.aerialViewBoundsUpperPoint [1]) / 2 - MIN_DEPTH / 2; this.aerialViewBoundsUpperPoint [1] = this.aerialViewBoundsLowerPoint [1] + MIN_DEPTH; } if (MIN_HEIGHT > this.aerialViewBoundsUpperPoint [2] - this.aerialViewBoundsLowerPoint [2]) { this.aerialViewBoundsLowerPoint [2] = (this.aerialViewBoundsLowerPoint [2] + this.aerialViewBoundsUpperPoint [2]) / 2 - MIN_HEIGHT / 2; this.aerialViewBoundsUpperPoint [2] = this.aerialViewBoundsLowerPoint [2] + MIN_HEIGHT; } } } /** * Adds the point at the given coordinates to aerial view bounds. */ private void updateAerialViewBounds(float x, float y, float minZ, float maxZ) { if (this.aerialViewBoundsLowerPoint == null) { this.aerialViewBoundsLowerPoint = new float [] {x, y, minZ}; this.aerialViewBoundsUpperPoint = new float [] {x, y, maxZ}; } else { this.aerialViewBoundsLowerPoint [0] = Math.min(this.aerialViewBoundsLowerPoint [0], x); this.aerialViewBoundsUpperPoint [0] = Math.max(this.aerialViewBoundsUpperPoint [0], x); this.aerialViewBoundsLowerPoint [1] = Math.min(this.aerialViewBoundsLowerPoint [1], y); this.aerialViewBoundsUpperPoint [1] = Math.max(this.aerialViewBoundsUpperPoint [1], y); this.aerialViewBoundsLowerPoint [2] = Math.min(this.aerialViewBoundsLowerPoint [2], minZ); this.aerialViewBoundsUpperPoint [2] = Math.max(this.aerialViewBoundsUpperPoint [2], maxZ); } } /** * Returns <code>true</code> if the given <code>item</code> is at a visible level. */ private boolean isItemAtVisibleLevel(Elevatable item) { return item.getLevel() == null || item.getLevel().isVisible(); } /** * Updates the minimum and maximum distances of the camera to the center of the aerial view. */ private void updateCameraIntervalToAerialViewCenter() { float homeBoundsWidth = this.aerialViewBoundsUpperPoint [0] - this.aerialViewBoundsLowerPoint [0]; float homeBoundsDepth = this.aerialViewBoundsUpperPoint [1] - this.aerialViewBoundsLowerPoint [1]; float homeBoundsHeight = this.aerialViewBoundsUpperPoint [2] - this.aerialViewBoundsLowerPoint [2]; float halfDiagonal = (float)Math.sqrt(homeBoundsWidth * homeBoundsWidth + homeBoundsDepth * homeBoundsDepth + homeBoundsHeight * homeBoundsHeight) / 2; this.minDistanceToAerialViewCenter = halfDiagonal * 1.05f; this.maxDistanceToAerialViewCenter = Math.max(5 * this.minDistanceToAerialViewCenter, 1000); } @Override public void moveCamera(float delta) { // Use a 5 times bigger delta for top camera move delta *= 5; float newDistanceToCenter = getCameraToAerialViewCenterDistance() - delta; placeCameraAt(newDistanceToCenter, false); } public void placeCameraAt(float distanceToCenter, boolean firstPieceOfFurnitureAddedToEmptyHome) { // Check camera is always outside the sphere centered in home center and with a radius equal to minimum distance distanceToCenter = Math.max(distanceToCenter, this.minDistanceToAerialViewCenter); // Check camera isn't too far distanceToCenter = Math.min(distanceToCenter, this.maxDistanceToAerialViewCenter); if (firstPieceOfFurnitureAddedToEmptyHome) { // Get closer to the first piece of furniture added to an empty home when that is small distanceToCenter = Math.min(distanceToCenter, 3 * this.minDistanceToAerialViewCenter); } double distanceToCenterAtGroundLevel = distanceToCenter * Math.cos(this.topCamera.getPitch()); this.topCamera.setX((this.aerialViewBoundsLowerPoint [0] + this.aerialViewBoundsUpperPoint [0]) / 2 + (float)(Math.sin(this.topCamera.getYaw()) * distanceToCenterAtGroundLevel)); this.topCamera.setY((this.aerialViewBoundsLowerPoint [1] + this.aerialViewBoundsUpperPoint [1]) / 2 - (float)(Math.cos(this.topCamera.getYaw()) * distanceToCenterAtGroundLevel)); this.topCamera.setZ((this.aerialViewBoundsLowerPoint [2] + this.aerialViewBoundsUpperPoint [2]) / 2 + (float)Math.sin(this.topCamera.getPitch()) * distanceToCenter); } @Override public void rotateCameraYaw(float delta) { float newYaw = this.topCamera.getYaw() + delta; double distanceToCenterAtGroundLevel = getCameraToAerialViewCenterDistance() * Math.cos(this.topCamera.getPitch()); // Change camera yaw and location so user turns around home this.topCamera.setYaw(newYaw); this.topCamera.setX((this.aerialViewBoundsLowerPoint [0] + this.aerialViewBoundsUpperPoint [0]) / 2 + (float)(Math.sin(newYaw) * distanceToCenterAtGroundLevel)); this.topCamera.setY((this.aerialViewBoundsLowerPoint [1] + this.aerialViewBoundsUpperPoint [1]) / 2 - (float)(Math.cos(newYaw) * distanceToCenterAtGroundLevel)); } @Override public void rotateCameraPitch(float delta) { float newPitch = this.topCamera.getPitch() + delta; // Check new pitch is between 0 and PI / 2 newPitch = Math.max(newPitch, (float)0); newPitch = Math.min(newPitch, (float)Math.PI / 2); // Compute new z to keep the same distance to view center double distanceToCenter = getCameraToAerialViewCenterDistance(); double distanceToCenterAtGroundLevel = distanceToCenter * Math.cos(newPitch); // Change camera pitch this.topCamera.setPitch(newPitch); this.topCamera.setX((this.aerialViewBoundsLowerPoint [0] + this.aerialViewBoundsUpperPoint [0]) / 2 + (float)(Math.sin(this.topCamera.getYaw()) * distanceToCenterAtGroundLevel)); this.topCamera.setY((this.aerialViewBoundsLowerPoint [1] + this.aerialViewBoundsUpperPoint [1]) / 2 - (float)(Math.cos(this.topCamera.getYaw()) * distanceToCenterAtGroundLevel)); this.topCamera.setZ((this.aerialViewBoundsLowerPoint [2] + this.aerialViewBoundsUpperPoint [2]) / 2 + (float)(distanceToCenter * Math.sin(newPitch))); } @Override public void goToCamera(Camera camera) { this.topCamera.setCamera(camera); this.topCamera.setTime(camera.getTime()); this.topCamera.setLens(camera.getLens()); updateCameraFromHomeBounds(false); } @Override public void exit() { this.topCamera = null; for (Wall wall : home.getWalls()) { wall.removePropertyChangeListener(this.objectChangeListener); } home.removeWallsListener(wallsListener); for (HomePieceOfFurniture piece : home.getFurniture()) { piece.removePropertyChangeListener(this.objectChangeListener); } home.removeFurnitureListener(this.furnitureListener); for (Room room : home.getRooms()) { room.removePropertyChangeListener(this.objectChangeListener); } home.removeRoomsListener(this.roomsListener); for (Level level : home.getLevels()) { level.removePropertyChangeListener(this.objectChangeListener); } home.removeLevelsListener(this.levelsListener); home.removeSelectionListener(this.selectionListener); } } /** * Preferences property listener bound to top camera state with a weak reference to avoid * strong link between user preferences and top camera state. */ private static class UserPreferencesChangeListener implements PropertyChangeListener { private WeakReference<TopCameraState> topCameraState; public UserPreferencesChangeListener(TopCameraState topCameraState) { this.topCameraState = new WeakReference<TopCameraState>(topCameraState); } public void propertyChange(PropertyChangeEvent ev) { // If top camera state was garbage collected, remove this listener from preferences TopCameraState topCameraState = this.topCameraState.get(); UserPreferences preferences = (UserPreferences)ev.getSource(); if (topCameraState == null) { preferences.removePropertyChangeListener(UserPreferences.Property.valueOf(ev.getPropertyName()), this); } else { topCameraState.setAerialViewCenteredOnSelectionEnabled(preferences.isAerialViewCenteredOnSelectionEnabled()); } } } /** * Observer camera controller state. */ private class ObserverCameraState extends CameraControllerState { private ObserverCamera observerCamera; private PropertyChangeListener levelElevationChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent ev) { if (Level.Property.ELEVATION.name().equals(ev.getPropertyName())) { updateCameraMinimumElevation(); } } }; private CollectionListener<Level> levelsListener = new CollectionListener<Level>() { public void collectionChanged(CollectionEvent<Level> ev) { if (ev.getType() == CollectionEvent.Type.ADD) { ev.getItem().addPropertyChangeListener(levelElevationChangeListener); } else if (ev.getType() == CollectionEvent.Type.DELETE) { ev.getItem().removePropertyChangeListener(levelElevationChangeListener); } updateCameraMinimumElevation(); } }; @Override public void enter() { this.observerCamera = (ObserverCamera)home.getCamera(); for (Level level : home.getLevels()) { level.addPropertyChangeListener(this.levelElevationChangeListener); } home.addLevelsListener(this.levelsListener); // Select observer camera for user feedback home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); } @Override public void moveCamera(float delta) { this.observerCamera.setX(this.observerCamera.getX() - (float)Math.sin(this.observerCamera.getYaw()) * delta); this.observerCamera.setY(this.observerCamera.getY() + (float)Math.cos(this.observerCamera.getYaw()) * delta); // Select observer camera for user feedback home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); } @Override public void elevateCamera(float delta) { float newElevation = this.observerCamera.getZ() + delta; newElevation = Math.min(Math.max(newElevation, getMinimumElevation()), preferences.getLengthUnit().getMaximumElevation()); this.observerCamera.setZ(newElevation); // Select observer camera for user feedback home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); } private void updateCameraMinimumElevation() { observerCamera.setZ(Math.max(observerCamera.getZ(), getMinimumElevation())); } public float getMinimumElevation() { List<Level> levels = home.getLevels(); if (levels.size() > 0) { return 10 + levels.get(0).getElevation(); } else { return 10; } } @Override public void rotateCameraYaw(float delta) { this.observerCamera.setYaw(this.observerCamera.getYaw() + delta); // Select observer camera for user feedback home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); } @Override public void rotateCameraPitch(float delta) { float newPitch = this.observerCamera.getPitch() + delta; // Check new angle is between -60� and 75� newPitch = Math.max(newPitch, -(float)Math.PI / 3); newPitch = Math.min(newPitch, (float)Math.PI / 36 * 15); this.observerCamera.setPitch(newPitch); // Select observer camera for user feedback home.setSelectedItems(Arrays.asList(new Selectable [] {this.observerCamera})); } @Override public void goToCamera(Camera camera) { this.observerCamera.setCamera(camera); this.observerCamera.setTime(camera.getTime()); this.observerCamera.setLens(camera.getLens()); } @Override public void exit() { // Remove observer camera from selection List<Selectable> selectedItems = home.getSelectedItems(); if (selectedItems.contains(this.observerCamera)) { selectedItems = new ArrayList<Selectable>(selectedItems); selectedItems.remove(this.observerCamera); home.setSelectedItems(selectedItems); } for (Level room : home.getLevels()) { room.removePropertyChangeListener(this.levelElevationChangeListener); } home.removeLevelsListener(this.levelsListener); this.observerCamera = null; } } }