/*
* PhotosPanel.java 5 Nov 2012
*
* Sweet Home 3D, Copyright (c) 2012 Emmanuel PUYBARET / eTeks <info@eteks.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.eteks.sweethome3d.swing;
import java.awt.CardLayout;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import javax.swing.AbstractAction;
import javax.swing.AbstractListModel;
import javax.swing.ActionMap;
import javax.swing.BoundedRangeModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.eteks.sweethome3d.j3d.PhotoRenderer;
import com.eteks.sweethome3d.model.Camera;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.Selectable;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.viewcontroller.ContentManager;
import com.eteks.sweethome3d.viewcontroller.DialogView;
import com.eteks.sweethome3d.viewcontroller.PhotosController;
import com.eteks.sweethome3d.viewcontroller.View;
/**
* A panel to edit photos created at home points of view.
* @author Emmanuel Puybaret
*/
public class PhotosPanel extends JPanel implements DialogView {
private enum ActionType {START_PHOTOS_CREATION, STOP_PHOTOS_CREATION, CLOSE}
private static final String PHOTOS_DIALOG_X_VISUAL_PROPERTY = "com.eteks.sweethome3d.swing.PhotosPanel.PhotoDialogX";
private static final String PHOTOS_DIALOG_Y_VISUAL_PROPERTY = "com.eteks.sweethome3d.swing.PhotosPanel.PhotoDialogY";
private static final String TIP_CARD = "tip";
private static final String PROGRESS_CARD = "progress";
private static final String END_CARD = "end";
private final Home home;
private final UserPreferences preferences;
private final PhotosController controller;
private JLabel selectedCamerasLabel;
private JList selectedCamerasList;
private CardLayout statusLayout;
private JPanel statusPanel;
private JLabel tipLabel;
private JLabel progressLabel;
private JProgressBar progressBar;
private JLabel endLabel;
private ScaledImageComponent photoComponent;
private PhotoSizeAndQualityPanel sizeAndQualityPanel;
private JLabel fileFormatLabel;
private JComboBox fileFormatComboBox;
private String dialogTitle;
private ExecutorService photosCreationExecutor;
private JButton startStopButton;
private JButton closeButton;
private static PhotosPanel currentPhotosPanel; // Support only one photos panel opened at a time
public PhotosPanel(Home home,
UserPreferences preferences,
PhotosController controller) {
super(new GridBagLayout());
this.home = home;
this.preferences = preferences;
this.controller = controller;
createActions(preferences);
createComponents(home, preferences, controller);
setMnemonics(preferences);
layoutComponents();
preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE, new LanguageChangeListener(this));
}
/**
* Creates actions for variables.
*/
private void createActions(UserPreferences preferences) {
final ActionMap actions = getActionMap();
actions.put(ActionType.START_PHOTOS_CREATION,
new ResourceAction(preferences, PhotosPanel.class, ActionType.START_PHOTOS_CREATION.name(), true) {
@Override
public void actionPerformed(ActionEvent ev) {
startPhotosCreation();
}
});
actions.put(ActionType.STOP_PHOTOS_CREATION,
new ResourceAction(preferences, PhotosPanel.class, ActionType.STOP_PHOTOS_CREATION.name(), true) {
@Override
public void actionPerformed(ActionEvent ev) {
stopPhotosCreation();
}
});
actions.put(ActionType.CLOSE,
new ResourceAction(preferences, PhotosPanel.class, ActionType.CLOSE.name(), true) {
@Override
public void actionPerformed(ActionEvent ev) {
close();
}
});
}
/**
* Creates and initializes components.
*/
private void createComponents(final Home home,
final UserPreferences preferences,
final PhotosController controller) {
// Create selected cameras label and list bound to SELECTED_CAMERAS controller property
this.selectedCamerasLabel = new JLabel();
this.selectedCamerasList = new JList(new CamerasListModel());
this.selectedCamerasList.setCellRenderer(new ListCellRenderer() {
private JCheckBox cameraCheckBox = new JCheckBox();
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
boolean cellHasFocus) {
this.cameraCheckBox.setText(((Camera)value).getName());
this.cameraCheckBox.setSelected(controller.getSelectedCameras().contains(value));
this.cameraCheckBox.setOpaque(true);
if (isSelected && list.hasFocus()) {
this.cameraCheckBox.setBackground(list.getSelectionBackground());
this.cameraCheckBox.setForeground(list.getSelectionForeground());
}
else {
this.cameraCheckBox.setBackground(list.getBackground());
this.cameraCheckBox.setForeground(list.getForeground());
}
return this.cameraCheckBox;
}
});
this.selectedCamerasList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent ev) {
if (selectedCamerasList.isEnabled()) {
int index = selectedCamerasList.locationToIndex(ev.getPoint());
if (index >= 0) {
toggleCameraSelection((Camera)selectedCamerasList.getModel().getElementAt(index), controller);
}
}
}
});
this.selectedCamerasList.getInputMap().put(KeyStroke.getKeyStroke("pressed SPACE"), "toggleSelection");
this.selectedCamerasList.getActionMap().put("toggleSelection", new AbstractAction() {
public void actionPerformed(ActionEvent ev) {
if (selectedCamerasList.isEnabled()) {
Camera camera = (Camera)selectedCamerasList.getSelectedValue();
if (camera != null) {
toggleCameraSelection(camera, controller);
}
}
}
});
this.selectedCamerasList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
this.selectedCamerasList.setSelectedIndex(0);
controller.addPropertyChangeListener(PhotosController.Property.SELECTED_CAMERAS, new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
selectedCamerasList.repaint();
getActionMap().get(ActionType.START_PHOTOS_CREATION).setEnabled(!((List)ev.getNewValue()).isEmpty());
statusLayout.show(statusPanel, TIP_CARD);
}
});
controller.addPropertyChangeListener(PhotosController.Property.CAMERAS, new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
selectedCamerasList.repaint();
}
});
// Create tip / progress / end components
this.tipLabel = new JLabel();
Font toolTipFont = UIManager.getFont("ToolTip.font");
this.tipLabel.setFont(toolTipFont);
this.progressLabel = new JLabel();
this.progressLabel.setFont(toolTipFont);
this.progressLabel.setHorizontalAlignment(JLabel.CENTER);
this.progressBar = new JProgressBar();
this.progressBar.setIndeterminate(true);
this.progressBar.getModel().addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
int progressValue = progressBar.getValue();
progressBar.setIndeterminate(progressValue < 0);
if (progressValue >= 0) {
progressLabel.setText(preferences.getLocalizedString(PhotosPanel.class, "progressLabel.format",
progressValue + 1, progressBar.getMaximum()));
}
}
});
this.endLabel = new JLabel();
this.endLabel.setFont(toolTipFont);
this.endLabel.setHorizontalAlignment(JLabel.CENTER);
this.photoComponent = new ScaledImageComponent();
this.photoComponent.setPreferredSize(new Dimension(toolTipFont.getSize() * 5, toolTipFont.getSize() * 5));
// Create size and quality panel
this.sizeAndQualityPanel = new PhotoSizeAndQualityPanel(home, preferences, controller);
// Create file format label and combo box bound to FILE_FORMAT / FILE_COMPRESSION_QUALITY controller properties
this.fileFormatLabel = new JLabel();
this.fileFormatComboBox = new JComboBox(new Object [] {"PNG", "JPEG 0.3", "JPEG 0.5", "JPEG 0.7", "JPEG 0.9"});
this.fileFormatComboBox.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
String string = (String)value;
String displayedValue = "";
if ("PNG".equals(string)) {
displayedValue = preferences.getLocalizedString(
PhotosPanel.class, "fileFormatComboBox.png.text");
} else if (((String)string).startsWith("JPEG")) {
float compressionQuality = Float.parseFloat(string.substring(string.lastIndexOf(' ') + 1));
displayedValue = preferences.getLocalizedString(
PhotosPanel.class, "fileFormatComboBox.jpeg.text", Math.round(compressionQuality * 100));
}
return super.getListCellRendererComponent(list, displayedValue, index, isSelected,
cellHasFocus);
}
});
ItemListener fileFormatItemListener = new ItemListener() {
public void itemStateChanged(ItemEvent ev) {
String value = (String)fileFormatComboBox.getSelectedItem();
if (value.startsWith("JPEG")) {
controller.setFileFormat("JPEG");
controller.setFileCompressionQuality(new Float(value.substring(value.lastIndexOf(' ') + 1)));
} else {
controller.setFileFormat("PNG");
controller.setFileCompressionQuality(null);
}
}
};
this.fileFormatComboBox.addItemListener(fileFormatItemListener);
PropertyChangeListener fileFormatChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
fileFormatComboBox.setSelectedItem(controller.getFileFormat()
+ (controller.getFileCompressionQuality() != null ? " " + controller.getFileCompressionQuality() : ""));
}
};
controller.addPropertyChangeListener(PhotosController.Property.FILE_FORMAT, fileFormatChangeListener);
controller.addPropertyChangeListener(PhotosController.Property.FILE_COMPRESSION_QUALITY, fileFormatChangeListener);
if (controller.getFileFormat() != null) {
fileFormatChangeListener.propertyChange(null);
} else {
fileFormatItemListener.itemStateChanged(null);
}
final JComponent view3D = (JComponent)controller.get3DView();
controller.set3DViewAspectRatio((float)view3D.getWidth() / view3D.getHeight());
final ActionMap actionMap = getActionMap();
this.startStopButton = new JButton(actionMap.get(ActionType.START_PHOTOS_CREATION));
this.closeButton = new JButton(actionMap.get(ActionType.CLOSE));
setComponentTexts(preferences);
}
/**
* Toggles the selected status of the given <code>camera</code>.
*/
private void toggleCameraSelection(Camera camera, final PhotosController controller) {
List<Camera> selectedCameras = new ArrayList<Camera>(controller.getSelectedCameras());
if (selectedCameras.contains(camera)) {
selectedCameras.remove(camera);
} else {
selectedCameras.add(camera);
}
controller.setSelectedCameras(selectedCameras);
}
/**
* Sets the texts of the components.
*/
private void setComponentTexts(UserPreferences preferences) {
this.tipLabel.setText(preferences.getLocalizedString(PhotosPanel.class, "tipLabel.text"));
this.endLabel.setText(preferences.getLocalizedString(PhotosPanel.class, "endLabel.text"));
this.selectedCamerasLabel.setText(SwingTools.getLocalizedLabelText(preferences,
PhotosPanel.class, "selectedCamerasLabel.text"));
this.fileFormatLabel.setText(SwingTools.getLocalizedLabelText(preferences,
PhotosPanel.class, "fileFormatLabel.text"));
this.dialogTitle = preferences.getLocalizedString(PhotosPanel.class, "createPhotos.title");
Window window = SwingUtilities.getWindowAncestor(this);
if (window != null) {
((JDialog)window).setTitle(this.dialogTitle);
}
// Buttons text changes automatically through their action
}
/**
* Sets components mnemonics and label / component associations.
*/
private void setMnemonics(UserPreferences preferences) {
if (!OperatingSystem.isMacOSX()) {
this.selectedCamerasLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
PhotosPanel.class, "selectedCamerasLabel.mnemonic")).getKeyCode());
this.selectedCamerasLabel.setLabelFor(this.selectedCamerasList);
this.fileFormatLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
PhotosPanel.class, "fileFormatLabel.mnemonic")).getKeyCode());
this.fileFormatLabel.setLabelFor(this.fileFormatComboBox);
}
}
/**
* Preferences property listener bound to this panel with a weak reference to avoid
* strong link between user preferences and this panel.
*/
public static class LanguageChangeListener implements PropertyChangeListener {
private final WeakReference<PhotosPanel> photoPanel;
public LanguageChangeListener(PhotosPanel photoPanel) {
this.photoPanel = new WeakReference<PhotosPanel>(photoPanel);
}
public void propertyChange(PropertyChangeEvent ev) {
// If photo panel was garbage collected, remove this listener from preferences
PhotosPanel photoPanel = this.photoPanel.get();
UserPreferences preferences = (UserPreferences)ev.getSource();
if (photoPanel == null) {
preferences.removePropertyChangeListener(UserPreferences.Property.LANGUAGE, this);
} else {
photoPanel.setComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
photoPanel.setComponentTexts(preferences);
photoPanel.setMnemonics(preferences);
}
}
}
/**
* Layouts panel components in panel with their labels.
*/
private void layoutComponents() {
int labelAlignment = OperatingSystem.isMacOSX()
? JLabel.TRAILING
: JLabel.LEADING;
// Add tip and progress bar to a card panel
this.statusLayout = new CardLayout();
this.statusPanel = new JPanel(this.statusLayout);
this.statusPanel.add(this.tipLabel, TIP_CARD);
this.tipLabel.setMinimumSize(this.tipLabel.getPreferredSize());
JPanel progressPanel = new JPanel(new GridBagLayout());
progressPanel.add(this.photoComponent, new GridBagConstraints(
0, 0, 1, 2, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
progressPanel.add(this.progressBar, new GridBagConstraints(
1, 0, 1, 1, 1, 1, GridBagConstraints.SOUTH,
GridBagConstraints.HORIZONTAL, new Insets(0, 5, 5, 0), 0, 0));
progressPanel.add(this.progressLabel, new GridBagConstraints(
1, 1, 1, 1, 0, 1, GridBagConstraints.NORTH,
GridBagConstraints.NONE, new Insets(0, 5, 0, 0), 0, 0));
this.statusPanel.add(progressPanel, PROGRESS_CARD);
this.statusPanel.add(this.endLabel, END_CARD);
this.endLabel.setMinimumSize(this.endLabel.getPreferredSize());
// First row
add(this.selectedCamerasLabel, new GridBagConstraints(
0, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
// Second row
add(SwingTools.createScrollPane(this.selectedCamerasList), new GridBagConstraints(
0, 1, 1, 1, 1, 1, GridBagConstraints.CENTER,
GridBagConstraints.BOTH, new Insets(0, 0, 5, 0), 0, 0));
// Third row
add(this.statusPanel, new GridBagConstraints(
0, 2, 1, 1, 1, 0, GridBagConstraints.CENTER,
GridBagConstraints.NONE, new Insets(0, 0, 10, 0), 0, 0));
// Fourth row
add(this.sizeAndQualityPanel, new GridBagConstraints(
0, 3, 1, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
// Last row
JPanel fileFormatPanel = new JPanel();
fileFormatPanel.add(this.fileFormatLabel);
fileFormatPanel.add(this.fileFormatComboBox);
this.fileFormatLabel.setHorizontalAlignment(labelAlignment);
add(fileFormatPanel, new GridBagConstraints(
0, 7, 4, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.NONE, new Insets(10, 0, 0, 0), 0, 0));
}
/**
* Displays this panel in a non modal dialog.
*/
public void displayView(View parentView) {
if (currentPhotosPanel == this) {
SwingUtilities.getWindowAncestor(PhotosPanel.this).toFront();
} else {
if (currentPhotosPanel != null) {
currentPhotosPanel.close();
}
final JOptionPane optionPane = new JOptionPane(this,
JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION,
null, new Object [] {this.startStopButton, this.closeButton}, this.startStopButton);
if (parentView != null) {
optionPane.setComponentOrientation(((JComponent)parentView).getComponentOrientation());
}
final JDialog dialog = optionPane.createDialog(SwingUtilities.getRootPane((Component)parentView), this.dialogTitle);
dialog.setModal(false);
Component homeRoot = SwingUtilities.getRoot((Component)parentView);
Point dialogLocation = null;
if (homeRoot != null) {
// Restore location if it exists
Integer x = (Integer)this.home.getVisualProperty(PHOTOS_DIALOG_X_VISUAL_PROPERTY);
Integer y = (Integer)this.home.getVisualProperty(PHOTOS_DIALOG_Y_VISUAL_PROPERTY);
int windowRightBorder = homeRoot.getX() + homeRoot.getWidth();
Dimension screenSize = getToolkit().getScreenSize();
Insets screenInsets = getToolkit().getScreenInsets(getGraphicsConfiguration());
int screenRightBorder = screenSize.width - screenInsets.right;
// Check dialog isn't too high
int screenHeight = screenSize.height - screenInsets.top - screenInsets.bottom;
if (OperatingSystem.isLinux() && screenHeight == screenSize.height) {
// Let's consider that under Linux at least an horizontal bar exists
screenHeight -= 30;
}
int screenBottomBorder = screenSize.height - screenInsets.bottom;
int dialogWidth = dialog.getWidth();
if (dialog.getHeight() > screenHeight) {
dialog.setSize(dialogWidth, screenHeight);
}
int dialogHeight = dialog.getHeight();
if (x != null && y != null
&& x + dialogWidth <= screenRightBorder
&& y + dialogHeight <= screenBottomBorder) {
dialogLocation = new Point(x, y);
} else if (screenRightBorder - windowRightBorder > dialogWidth / 2
|| dialogHeight == screenHeight) {
// If there some space left at the right of the window,
// move the dialog to the right of window
dialogLocation = new Point(Math.min(windowRightBorder + 5, screenRightBorder - dialogWidth),
Math.max(Math.min(homeRoot.getY(), screenSize.height - dialogHeight - screenInsets.bottom), screenInsets.top));
}
}
if (dialogLocation != null
&& SwingTools.isRectangleVisibleAtScreen(new Rectangle(dialogLocation, dialog.getSize()))) {
dialog.setLocationByPlatform(false);
dialog.setLocation(dialogLocation);
} else {
dialog.setLocationByPlatform(true);
}
dialog.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent ev) {
stopPhotosCreation();
currentPhotosPanel = null;
}
});
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent ev) {
if (optionPane.getValue() != null
&& optionPane.getValue() != JOptionPane.UNINITIALIZED_VALUE) {
close();
}
}
@Override
public void componentMoved(ComponentEvent ev) {
controller.setVisualProperty(PHOTOS_DIALOG_X_VISUAL_PROPERTY, dialog.getX());
controller.setVisualProperty(PHOTOS_DIALOG_Y_VISUAL_PROPERTY, dialog.getY());
}
});
dialog.setVisible(true);
currentPhotosPanel = this;
}
}
/**
* Creates the photo images in a folder depending on the quality requested by the user.
*/
private void startPhotosCreation() {
final String directory = this.controller.getContentManager().showSaveDialog(this,
this.preferences.getLocalizedString(PhotosPanel.class, "selectPhotosFolderDialog.title"),
ContentManager.ContentType.PHOTOS_DIRECTORY, this.home.getName());
if (directory != null) {
// Build file names list
final Map<Camera, File> cameraFiles = new LinkedHashMap<Camera, File>();
List<Camera> selectedCameras = this.controller.getSelectedCameras();
ContentManager contentManager = this.controller.getContentManager();
boolean overwriteConfirmed = false;
for (Camera camera : this.controller.getCameras()) {
if (selectedCameras.contains(camera)) {
String fileName = "";
if (this.home.getName() != null) {
fileName += contentManager.getPresentationName(this.home.getName(), ContentManager.ContentType.SWEET_HOME_3D);
if (contentManager instanceof FileContentManager) {
fileName = fileName.substring(0,
fileName.length()
- ((FileContentManager)contentManager).getDefaultFileExtension(
ContentManager.ContentType.SWEET_HOME_3D).length());
}
fileName += " - ";
}
fileName += camera.getName();
fileName = fileName.replaceAll("/|\\\\|:|;", "-").replace(File.pathSeparatorChar, '-').replace(File.separatorChar, '-');
if (contentManager instanceof FileContentManager) {
fileName += ((FileContentManager)contentManager).getDefaultFileExtension(
ContentManager.ContentType.valueOf(controller.getFileFormat()));
}
File cameraFile = new File(directory, fileName);
if (!overwriteConfirmed && cameraFile.exists()) {
if (JOptionPane.showConfirmDialog(this,
this.preferences.getLocalizedString(PhotosPanel.class, "confirmOverwrite.message", directory),
this.preferences.getLocalizedString(PhotosPanel.class, "confirmOverwrite.title"), JOptionPane.YES_NO_OPTION)
== JOptionPane.NO_OPTION) {
return;
} else {
overwriteConfirmed = true;
}
}
cameraFiles.put(camera, cameraFile);
}
}
this.photoComponent.setImage(null);
this.selectedCamerasList.setEnabled(false);
this.sizeAndQualityPanel.setEnabled(false);
this.fileFormatComboBox.setEnabled(false);
getRootPane().setDefaultButton(this.startStopButton);
this.startStopButton.setAction(getActionMap().get(ActionType.STOP_PHOTOS_CREATION));
this.statusLayout.show(this.statusPanel, PROGRESS_CARD);
this.progressBar.setMinimum(-1);
this.progressBar.setValue(-1);
this.progressLabel.setText("");
this.photoComponent.setImage(null);
// Compute photos in an other executor thread
// Use a clone of home because the user can modify home during photos computation
final Home home = this.home.clone();
List<Selectable> emptySelection = Collections.emptyList();
home.setSelectedItems(emptySelection);
this.photosCreationExecutor = Executors.newSingleThreadExecutor();
this.photosCreationExecutor.execute(new Runnable() {
public void run() {
computePhotos(home, cameraFiles);
}
});
}
}
/**
* Computes the photo of the given home.
* Caution : this method must be thread safe because it's called from an executor.
*/
private void computePhotos(Home home, final Map<Camera, File> cameraFiles) {
BufferedImage image = null;
boolean success = false;
try {
int photoIndex = 0;
for (Map.Entry<Camera, File> cameraEntry : cameraFiles.entrySet()) {
int quality = this.controller.getQuality();
int imageWidth = this.controller.getWidth();
int imageHeight = this.controller.getHeight();
Camera camera = cameraEntry.getKey();
home.setCamera(camera);
if (quality >= 2) {
// Use photo renderer
PhotoRenderer photoRenderer = new PhotoRenderer(home,
quality == 2
? PhotoRenderer.Quality.LOW
: PhotoRenderer.Quality.HIGH);
int bestImageHeight;
// Update ratio if lens is fisheye or spherical
if (camera.getLens() == Camera.Lens.FISHEYE) {
bestImageHeight = imageWidth;
} else if (camera.getLens() == Camera.Lens.SPHERICAL) {
bestImageHeight = imageWidth / 2;
} else {
bestImageHeight = imageHeight;
}
if (this.photosCreationExecutor != null) {
image = new BufferedImage(imageWidth, bestImageHeight, BufferedImage.TYPE_INT_RGB);
this.photoComponent.setImage(image);
updateProgressBar(photoIndex++, cameraFiles.size());
photoRenderer.render(image, camera, this.photoComponent);
}
} else {
// Compute 3D view offscreen image
HomeComponent3D homeComponent3D = new HomeComponent3D(home, this.preferences, quality == 1);
updateProgressBar(photoIndex++, cameraFiles.size());
image = homeComponent3D.getOffScreenImage(imageWidth, imageHeight);
this.photoComponent.setImage(image);
}
try {
if (this.photosCreationExecutor != null) {
savePhoto(image, cameraEntry.getValue());
}
} catch (final IOException ex) {
showPhotoSaveError(ex);
return;
}
if (this.photosCreationExecutor == null) {
return;
}
}
success = true;
} catch (OutOfMemoryError ex) {
showPhotosComputingError(ex);
} catch (IllegalStateException ex) {
showPhotosComputingError(ex);
} catch (IOException ex) {
showPhotosComputingError(ex);
} finally {
final boolean succeeded = success;
EventQueue.invokeLater(new Runnable() {
public void run() {
startStopButton.setAction(getActionMap().get(ActionType.START_PHOTOS_CREATION));
selectedCamerasList.setEnabled(true);
sizeAndQualityPanel.setEnabled(true);
sizeAndQualityPanel.setProportionsChoiceEnabled(true);
fileFormatComboBox.setEnabled(true);
if (succeeded) {
statusLayout.show(statusPanel, END_CARD);
} else {
statusLayout.show(statusPanel, TIP_CARD);
}
photosCreationExecutor = null;
}
});
}
}
private void updateProgressBar(final int photoIndex, final int photoCount) {
final BoundedRangeModel progressModel = this.progressBar.getModel();
EventQueue.invokeLater(new Runnable() {
public void run() {
progressModel.setMinimum(0);
progressModel.setMaximum(photoCount);
progressModel.setValue(photoIndex);
}
});
}
/**
* Displays an error message box for save errors.
*/
private void showPhotoSaveError(final Throwable ex) {
try {
EventQueue.invokeAndWait(new Runnable() {
public void run() {
String messageFormat = preferences.getLocalizedString(PhotosPanel.class, "savePhotosError.message");
JOptionPane.showMessageDialog(SwingUtilities.getRootPane(PhotosPanel.this), String.format(messageFormat, ex.getMessage()),
preferences.getLocalizedString(PhotosPanel.class, "savePhotosError.title"), JOptionPane.ERROR_MESSAGE);
}
});
} catch (InterruptedException ex1) {
ex1.printStackTrace();
} catch (InvocationTargetException ex1) {
throw new RuntimeException(ex1);
}
}
/**
* Displays an error message box.
*/
public void showPhotosComputingError(Throwable exception) {
EventQueue.invokeLater(new Runnable() {
public void run() {
String title = preferences.getLocalizedString(PhotosPanel.class, "error.title");
String message = preferences.getLocalizedString(PhotosPanel.class, "error.message");
JOptionPane.showMessageDialog(PhotosPanel.this, message, title, JOptionPane.ERROR_MESSAGE);
}
});
}
/**
* Stops photos creation.
*/
private void stopPhotosCreation() {
if (this.photosCreationExecutor != null) {
// Will interrupt executor thread
this.photosCreationExecutor.shutdownNow();
this.photosCreationExecutor = null;
this.startStopButton.setAction(getActionMap().get(ActionType.START_PHOTOS_CREATION));
}
}
/**
* Saves the given <code>image</code>.
*/
private void savePhoto(BufferedImage image, File file) throws IOException {
String fileFormat = this.controller.getFileFormat();
Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(fileFormat);
ImageWriter writer = (ImageWriter)iter.next();
ImageWriteParam writeParam = writer.getDefaultWriteParam();
if ("JPEG".equals(fileFormat)) {
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
writeParam.setCompressionQuality(this.controller.getFileCompressionQuality());
}
FileImageOutputStream output = new FileImageOutputStream(file);
writer.setOutput(output);
writer.write(null, new IIOImage(image, null, null), writeParam);
writer.dispose();
output.close();
}
/**
* Manages closing of this pane.
*/
private void close() {
Window window = SwingUtilities.getWindowAncestor(this);
if (window.isDisplayable()) {
window.dispose();
}
}
/**
* List model for cameras.
*/
private final class CamerasListModel extends AbstractListModel {
private List<Camera> cameras;
public CamerasListModel() {
this.cameras = controller.getCameras();
controller.addPropertyChangeListener(PhotosController.Property.CAMERAS,
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
cameras = controller.getCameras();
fireContentsChanged(this, 0, cameras.size());
}
});
}
public int getSize() {
return this.cameras.size();
}
public Object getElementAt(int index) {
return this.cameras.get(index);
}
}
}