/*
* VideoPanel.java 15 fevr. 2010
*
* Sweet Home 3D, Copyright (c) 2010 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.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Composite;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
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.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Dictionary;
import java.util.GregorianCalendar;
import java.util.Hashtable;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.imageio.ImageIO;
import javax.media.Buffer;
import javax.media.Format;
import javax.media.MediaLocator;
import javax.media.Time;
import javax.media.format.VideoFormat;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.PullBufferDataSource;
import javax.media.protocol.PullBufferStream;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.BoundedRangeModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.ImageIcon;
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.JSeparator;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SpinnerDateModel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.vecmath.Point3f;
import com.eteks.sweethome3d.j3d.Component3DManager;
import com.eteks.sweethome3d.j3d.PhotoRenderer;
import com.eteks.sweethome3d.model.AspectRatio;
import com.eteks.sweethome3d.model.Camera;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.ObserverCamera;
import com.eteks.sweethome3d.model.Selectable;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.tools.ResourceURLContent;
import com.eteks.sweethome3d.viewcontroller.ContentManager;
import com.eteks.sweethome3d.viewcontroller.DialogView;
import com.eteks.sweethome3d.viewcontroller.VideoController;
import com.eteks.sweethome3d.viewcontroller.View;
/**
* A panel used for video creation.
* @author Emmanuel Puybaret
*/
public class VideoPanel extends JPanel implements DialogView {
private enum ActionType {START_VIDEO_CREATION, STOP_VIDEO_CREATION, SAVE_VIDEO, CLOSE,
DELETE_CAMERA_PATH, PLAYBACK, PAUSE, RECORD, SEEK_BACKWARD, SEEK_FORWARD, SKIP_BACKWARD, SKIP_FORWARD, DELETE_LAST_RECORD}
private static final String VIDEO_DIALOG_X_VISUAL_PROPERTY = "com.eteks.sweethome3d.swing.VideoPanel.VideoDialogX";
private static final String VIDEO_DIALOG_Y_VISUAL_PROPERTY = "com.eteks.sweethome3d.swing.VideoPanel.VideoDialogY";
private static final int MINIMUM_DELAY_BEFORE_DISCARDING_WITHOUT_WARNING = 30000;
private static final VideoFormat [] VIDEO_FORMATS = {
new VideoFormat(VideoFormat.JPEG, new Dimension(176, 132), Format.NOT_SPECIFIED, Format.byteArray, 12), // 4/3
new VideoFormat(VideoFormat.JPEG, new Dimension(320, 240), Format.NOT_SPECIFIED, Format.byteArray, 25),
new VideoFormat(VideoFormat.JPEG, new Dimension(480, 360), Format.NOT_SPECIFIED, Format.byteArray, 25),
new VideoFormat(VideoFormat.JPEG, new Dimension(640, 480), Format.NOT_SPECIFIED, Format.byteArray, 25),
new VideoFormat(VideoFormat.JPEG, new Dimension(720, 540), Format.NOT_SPECIFIED, Format.byteArray, 25),
new VideoFormat(VideoFormat.JPEG, new Dimension(1024, 768), Format.NOT_SPECIFIED, Format.byteArray, 25),
new VideoFormat(VideoFormat.JPEG, new Dimension(1280, 960), Format.NOT_SPECIFIED, Format.byteArray, 25),
new VideoFormat(VideoFormat.JPEG, new Dimension(720, 405), Format.NOT_SPECIFIED, Format.byteArray, 25), // 16/9
new VideoFormat(VideoFormat.JPEG, new Dimension(1280, 720), Format.NOT_SPECIFIED, Format.byteArray, 25),
new VideoFormat(VideoFormat.JPEG, new Dimension(1920, 1080), Format.NOT_SPECIFIED, Format.byteArray, 25)};
private static final String TIP_CARD = "tip";
private static final String PROGRESS_CARD = "progress";
private final Home home;
private final UserPreferences preferences;
private final VideoController controller;
private PlanComponent planComponent;
private JToolBar videoToolBar;
private JButton playbackPauseButton;
private Timer playbackTimer;
private ListIterator<Camera> cameraPathIterator;
private CardLayout statusLayout;
private JPanel statusPanel;
private JLabel tipLabel;
private JLabel progressLabel;
private JProgressBar progressBar;
private JLabel videoFormatLabel;
private JComboBox videoFormatComboBox;
private String videoFormatComboBoxFormat;
private JLabel qualityLabel;
private JSlider qualitySlider;
private Component advancedComponentsSeparator;
private JLabel dateLabel;
private JSpinner dateSpinner;
private JLabel timeLabel;
private JSpinner timeSpinner;
private JLabel dayNightLabel;
private JCheckBox ceilingLightEnabledCheckBox;
private String dialogTitle;
private ExecutorService videoCreationExecutor;
private long videoCreationStartTime;
private File videoFile;
private JButton createButton;
private JButton saveButton;
private JButton closeButton;
private static VideoPanel currentVideoPanel; // Support only one video panel opened at a time
/**
* Creates a video panel.
*/
public VideoPanel(Home home,
UserPreferences preferences,
VideoController 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.PLAYBACK,
new ResourceAction(preferences, VideoPanel.class, ActionType.PLAYBACK.name()) {
@Override
public void actionPerformed(ActionEvent ev) {
playback();
}
});
actions.put(ActionType.PAUSE,
new ResourceAction(preferences, VideoPanel.class, ActionType.PAUSE.name(), true) {
@Override
public void actionPerformed(ActionEvent ev) {
pausePlayback();
}
});
actions.put(ActionType.RECORD,
new ResourceAction(preferences, VideoPanel.class, ActionType.RECORD.name(), true) {
@Override
public void actionPerformed(ActionEvent ev) {
recordCameraLocation();
}
});
actions.put(ActionType.SEEK_BACKWARD,
new ResourceAction(preferences, VideoPanel.class, ActionType.SEEK_BACKWARD.name()) {
@Override
public void actionPerformed(ActionEvent ev) {
seekBackward();
}
});
actions.put(ActionType.SEEK_FORWARD,
new ResourceAction(preferences, VideoPanel.class, ActionType.SEEK_FORWARD.name()) {
@Override
public void actionPerformed(ActionEvent ev) {
seekForward();
}
});
actions.put(ActionType.SKIP_BACKWARD,
new ResourceAction(preferences, VideoPanel.class, ActionType.SKIP_BACKWARD.name()) {
@Override
public void actionPerformed(ActionEvent ev) {
skipBackward();
}
});
actions.put(ActionType.SKIP_FORWARD,
new ResourceAction(preferences, VideoPanel.class, ActionType.SKIP_FORWARD.name()) {
@Override
public void actionPerformed(ActionEvent ev) {
skipForward();
}
});
actions.put(ActionType.DELETE_LAST_RECORD,
new ResourceAction(preferences, VideoPanel.class, ActionType.DELETE_LAST_RECORD.name()) {
@Override
public void actionPerformed(ActionEvent ev) {
deleteLastRecordedCameraLocation();
}
});
actions.put(ActionType.DELETE_CAMERA_PATH,
new ResourceAction(preferences, VideoPanel.class, ActionType.DELETE_CAMERA_PATH.name()) {
@Override
public void actionPerformed(ActionEvent ev) {
deleteCameraPath();
}
});
actions.put(ActionType.START_VIDEO_CREATION,
new ResourceAction(preferences, VideoPanel.class, ActionType.START_VIDEO_CREATION.name()) {
@Override
public void actionPerformed(ActionEvent ev) {
startVideoCreation();
}
});
actions.put(ActionType.STOP_VIDEO_CREATION,
new ResourceAction(preferences, VideoPanel.class, ActionType.STOP_VIDEO_CREATION.name(), true) {
@Override
public void actionPerformed(ActionEvent ev) {
stopVideoCreation(true);
}
});
actions.put(ActionType.SAVE_VIDEO,
new ResourceAction(preferences, VideoPanel.class, ActionType.SAVE_VIDEO.name()) {
@Override
public void actionPerformed(ActionEvent ev) {
saveVideo();
}
});
actions.put(ActionType.CLOSE,
new ResourceAction(preferences, VideoPanel.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 VideoController controller) {
final Dimension preferredSize = new Dimension(getToolkit().getScreenSize().width <= 1024 ? 324 : 404, 404);
this.planComponent = new PlanComponent(home, preferences, null) {
private void updateScale() {
if (getWidth() > 0 && getHeight() > 0) {
// Adapt scale to always view the home
float oldScale = getScale();
Dimension preferredSize = super.getPreferredSize();
Insets insets = getInsets();
float planWidth = (preferredSize.width - insets.left - insets.right) / oldScale;
float planHeight = (preferredSize.height - insets.top - insets.bottom) / oldScale;
setScale(Math.min((getWidth() - insets.left - insets.right) / planWidth,
(getHeight() - insets.top - insets.bottom) / planHeight));
}
}
@Override
public Dimension getPreferredSize() {
return preferredSize;
}
@Override
public void revalidate() {
super.revalidate();
updateScale();
}
@Override
public void setBounds(int x, int y, int width, int height) {
super.setBounds(x, y, width, height);
updateScale();
}
@Override
protected List<Selectable> getPaintedItems() {
List<Selectable> paintedItems = super.getPaintedItems();
// Take into account camera locations in plan bounds
for (Camera camera : controller.getCameraPath()) {
paintedItems.add(new ObserverCamera(camera.getX(), camera.getY(), camera.getZ(),
camera.getYaw(), camera.getPitch(), camera.getFieldOfView()));
}
return paintedItems;
}
@Override
protected Rectangle2D getItemBounds(Graphics g, Selectable item) {
if (item instanceof ObserverCamera) {
return new Rectangle2D.Float(((ObserverCamera)item).getX() - 1, ((ObserverCamera)item).getY() - 1, 2, 2);
} else {
return super.getItemBounds(g, item);
}
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2D = (Graphics2D)g;
g2D.setColor(getBackground());
g2D.fillRect(0, 0, getWidth(), getHeight());
super.paintComponent(g);
}
@Override
protected void paintHomeItems(Graphics g, float planScale, Color backgroundColor, Color foregroundColor,
PaintMode paintMode) throws InterruptedIOException {
Graphics2D g2D = (Graphics2D)g;
Composite oldComposite = g2D.getComposite();
g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.25f));
super.paintHomeItems(g, planScale, backgroundColor, foregroundColor, paintMode);
// Paint recorded camera path
g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1));
g2D.setColor(getSelectionColor());
float cameraCircleRadius = 7 / getScale();
Ellipse2D ellipse = new Ellipse2D.Float(-cameraCircleRadius, -cameraCircleRadius,
2 * cameraCircleRadius, 2 * cameraCircleRadius);
List<Camera> cameraPath = controller.getCameraPath();
for (int i = 0; i < cameraPath.size(); i++) {
Camera camera = cameraPath.get(i);
AffineTransform previousTransform = g2D.getTransform();
g2D.translate(camera.getX(), camera.getY());
g2D.rotate(camera.getYaw());
// Paint camera location
g2D.fill(ellipse);
// Paint field of sight angle
double sin = (float)Math.sin(camera.getFieldOfView() / 2);
double cos = (float)Math.cos(camera.getFieldOfView() / 2);
float xStartAngle = (float)(1.2f * cameraCircleRadius * sin);
float yStartAngle = (float)(1.2f * cameraCircleRadius * cos);
float xEndAngle = (float)(2.5f * cameraCircleRadius * sin);
float yEndAngle = (float)(2.5f * cameraCircleRadius * cos);
GeneralPath cameraFieldOfViewAngle = new GeneralPath();
g2D.setStroke(new BasicStroke(1 / getScale()));
cameraFieldOfViewAngle.moveTo(xStartAngle, yStartAngle);
cameraFieldOfViewAngle.lineTo(xEndAngle, yEndAngle);
cameraFieldOfViewAngle.moveTo(-xStartAngle, yStartAngle);
cameraFieldOfViewAngle.lineTo(-xEndAngle, yEndAngle);
g2D.draw(cameraFieldOfViewAngle);
g2D.setTransform(previousTransform);
if (i > 0) {
g2D.setStroke(new BasicStroke(2 / getScale()));
g2D.draw(new Line2D.Float(camera.getX(), camera.getY(),
cameraPath.get(i - 1).getX(), cameraPath.get(i - 1).getY()));
}
}
g2D.setComposite(oldComposite);
}
};
this.planComponent.setSelectedItemsOutlinePainted(false);
this.planComponent.setBackgroundPainted(false);
this.planComponent.setBorder(BorderFactory.createEtchedBorder());
this.controller.addPropertyChangeListener(VideoController.Property.CAMERA_PATH,
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
planComponent.revalidate();
updatePlaybackTimer();
}
});
// Create tool bar to play recorded animation in 3D view
this.videoToolBar = new JToolBar();
this.videoToolBar.setFloatable(false);
final ActionMap actionMap = getActionMap();
this.videoToolBar.add(actionMap.get(ActionType.DELETE_CAMERA_PATH));
this.videoToolBar.addSeparator();
this.videoToolBar.add(actionMap.get(ActionType.SKIP_BACKWARD));
this.videoToolBar.add(actionMap.get(ActionType.SEEK_BACKWARD));
this.videoToolBar.add(actionMap.get(ActionType.RECORD));
this.playbackPauseButton = new JButton(actionMap.get(ActionType.PLAYBACK));
this.videoToolBar.add(this.playbackPauseButton);
this.videoToolBar.add(actionMap.get(ActionType.SEEK_FORWARD));
this.videoToolBar.add(actionMap.get(ActionType.SKIP_FORWARD));
this.videoToolBar.addSeparator();
this.videoToolBar.add(actionMap.get(ActionType.DELETE_LAST_RECORD));
for (int i = 0; i < videoToolBar.getComponentCount(); i++) {
Component component = this.videoToolBar.getComponent(i);
if (component instanceof JButton) {
JButton button = (JButton)component;
button.setBorderPainted(true);
button.setFocusable(true);
}
}
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() {
private long timeAfterFirstImage;
public void stateChanged(ChangeEvent ev) {
int progressValue = progressBar.getValue();
progressBar.setIndeterminate(progressValue <= progressBar.getMinimum() + 1);
if (progressValue == progressBar.getMinimum()
|| progressValue == progressBar.getMaximum()) {
progressLabel.setText("");
if (progressValue == progressBar.getMinimum()) {
int framesCount = progressBar.getMaximum() - progressBar.getMinimum();
String progressLabelFormat = preferences.getLocalizedString(VideoPanel.class, "progressStartLabel.format");
progressLabel.setText(String.format(progressLabelFormat, framesCount,
formatDuration(framesCount * 1000 / controller.getFrameRate())));
}
} else if (progressValue == progressBar.getMinimum() + 1) {
this.timeAfterFirstImage = System.currentTimeMillis();
} else {
// Update progress label once the second image is generated
// (the first one can take more time because of initialization process)
String progressLabelFormat = preferences.getLocalizedString(VideoPanel.class, "progressLabel.format");
long estimatedRemainingTime = (System.currentTimeMillis() - this.timeAfterFirstImage)
/ (progressValue - 1 - progressBar.getMinimum())
* (progressBar.getMaximum() - progressValue - 1);
String estimatedRemainingTimeText = formatDuration(estimatedRemainingTime);
progressLabel.setText(String.format(progressLabelFormat,
progressValue, progressBar.getMaximum(), estimatedRemainingTimeText));
}
}
/**
* Returns a localized string of <code>duration</code> in millis.
*/
private String formatDuration(long duration) {
long durationInSeconds = duration / 1000;
if (duration - durationInSeconds * 1000 >= 500) {
durationInSeconds++;
}
String estimatedRemainingTimeText;
if (durationInSeconds < 60) {
estimatedRemainingTimeText = String.format(preferences.getLocalizedString(
VideoPanel.class, "seconds.format"), durationInSeconds);
} else if (durationInSeconds < 3600) {
estimatedRemainingTimeText = String.format(preferences.getLocalizedString(
VideoPanel.class, "minutesSeconds.format"), durationInSeconds / 60, durationInSeconds % 60);
} else {
long hours = durationInSeconds / 3600;
long minutes = (durationInSeconds % 3600) / 60;
estimatedRemainingTimeText = String.format(preferences.getLocalizedString(
VideoPanel.class, "hoursMinutes.format"), hours, minutes);
}
return estimatedRemainingTimeText;
}
});
// Create video format label and combo box bound to WIDTH, HEIGHT, ASPECT_RATIO and FRAME_RATE controller properties
this.videoFormatLabel = new JLabel();
this.videoFormatComboBox = new JComboBox(VIDEO_FORMATS);
this.videoFormatComboBox.setMaximumRowCount(VIDEO_FORMATS.length);
this.videoFormatComboBox.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
VideoFormat videoFormat = (VideoFormat)value;
String aspectRatio;
switch (getAspectRatio(videoFormat)) {
case RATIO_4_3 :
aspectRatio = "4/3";
break;
case RATIO_16_9 :
default :
aspectRatio = "16/9";
break;
}
Dimension videoSize = videoFormat.getSize();
String displayedValue = String.format(videoFormatComboBoxFormat, videoSize.width, videoSize.height,
aspectRatio, (int)videoFormat.getFrameRate());
return super.getListCellRendererComponent(list, displayedValue, index, isSelected,
cellHasFocus);
}
});
this.videoFormatComboBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent ev) {
controller.setWidth(((VideoFormat)videoFormatComboBox.getSelectedItem()).getSize().width);
controller.setAspectRatio(getAspectRatio((VideoFormat)videoFormatComboBox.getSelectedItem()));
controller.setFrameRate((int)((VideoFormat)videoFormatComboBox.getSelectedItem()).getFrameRate());
}
});
PropertyChangeListener propertyChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
videoFormatComboBox.setSelectedItem(controller.getAspectRatio());
}
};
controller.addPropertyChangeListener(VideoController.Property.WIDTH, propertyChangeListener);
controller.addPropertyChangeListener(VideoController.Property.HEIGHT, propertyChangeListener);
controller.addPropertyChangeListener(VideoController.Property.ASPECT_RATIO, propertyChangeListener);
controller.addPropertyChangeListener(VideoController.Property.FRAME_RATE, propertyChangeListener);
// Quality label and slider bound to QUALITY controller property
this.qualityLabel = new JLabel();
this.qualitySlider = new JSlider(1, controller.getQualityLevelCount()) {
@Override
public String getToolTipText(MouseEvent ev) {
float valueUnderMouse = getSliderValueAt(this, ev.getX(), preferences);
float valueToTick = valueUnderMouse - (float)Math.floor(valueUnderMouse);
if (valueToTick < 0.25f || valueToTick > 0.75f) {
// Display a tooltip that explains the different quality levels
return "<html><table><tr valign='middle'>"
+ "<td><img border='1' src='"
+ new ResourceURLContent(PhotoPanel.class, "resources/quality" + Math.round(valueUnderMouse - qualitySlider.getMinimum()) + ".jpg").getURL() + "'></td>"
+ "<td>" + preferences.getLocalizedString(VideoPanel.class, "quality" + Math.round(valueUnderMouse - qualitySlider.getMinimum()) + "DescriptionLabel.text") + "</td>"
+ "</tr></table>";
} else {
return null;
}
}
};
// Add a listener that displays also the tool tip when user clicks on the slider
this.qualitySlider.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(final MouseEvent ev) {
EventQueue.invokeLater(new Runnable() {
public void run() {
float valueUnderMouse = getSliderValueAt(qualitySlider, ev.getX(), preferences);
if (qualitySlider.getValue() == Math.round(valueUnderMouse)) {
ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
int initialDelay = toolTipManager.getInitialDelay();
toolTipManager.setInitialDelay(Math.min(initialDelay, 150));
toolTipManager.mouseMoved(ev);
toolTipManager.setInitialDelay(initialDelay);
}
}
});
}
});
this.qualitySlider.setPaintLabels(true);
this.qualitySlider.setPaintTicks(true);
this.qualitySlider.setMajorTickSpacing(1);
this.qualitySlider.setSnapToTicks(true);
final boolean offScreenImageSupported = Component3DManager.getInstance().isOffScreenImageSupported();
this.qualitySlider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
if (!offScreenImageSupported) {
// Can't support 2 first quality levels if offscreen image isn't supported
qualitySlider.setValue(Math.max(qualitySlider.getMinimum() + 2, qualitySlider.getValue()));
}
controller.setQuality(qualitySlider.getValue() - qualitySlider.getMinimum());
}
});
controller.addPropertyChangeListener(VideoController.Property.QUALITY,
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
qualitySlider.setValue(qualitySlider.getMinimum() + controller.getQuality());
updateAdvancedComponents();
}
});
this.qualitySlider.setValue(this.qualitySlider.getMinimum() + controller.getQuality());
this.advancedComponentsSeparator = new JSeparator();
// Create date and time labels and spinners bound to TIME controller property
Date time = new Date(Camera.convertTimeToTimeZone(controller.getTime(), TimeZone.getDefault().getID()));
this.dateLabel = new JLabel();
final SpinnerDateModel dateSpinnerModel = new SpinnerDateModel();
dateSpinnerModel.setValue(time);
this.dateSpinner = new JSpinner(dateSpinnerModel);
String datePattern = ((SimpleDateFormat)DateFormat.getDateInstance(DateFormat.SHORT)).toPattern();
if (datePattern.indexOf("yyyy") == -1) {
datePattern = datePattern.replace("yy", "yyyy");
}
JSpinner.DateEditor dateEditor = new JSpinner.DateEditor(this.dateSpinner, datePattern);
this.dateSpinner.setEditor(dateEditor);
SwingTools.addAutoSelectionOnFocusGain(dateEditor.getTextField());
this.timeLabel = new JLabel();
final SpinnerDateModel timeSpinnerModel = new SpinnerDateModel();
timeSpinnerModel.setValue(time);
this.timeSpinner = new JSpinner(timeSpinnerModel);
// From http://en.wikipedia.org/wiki/12-hour_clock#Use_by_country
String [] twelveHoursCountries = {
"AU", // Australia
"BD", // Bangladesh
"CA", // Canada (excluding Quebec, in French)
"CO", // Colombia
"EG", // Egypt
"HN", // Honduras
"JO", // Jordan
"MX", // Mexico
"MY", // Malaysia
"NI", // Nicaragua
"NZ", // New Zealand
"PH", // Philippines
"PK", // Pakistan
"SA", // Saudi Arabia
"SV", // El Salvador
"US", // United States
"VE"}; // Venezuela
SimpleDateFormat timeInstance;
if ("en".equals(Locale.getDefault().getLanguage())) {
if (Arrays.binarySearch(twelveHoursCountries, Locale.getDefault().getCountry()) >= 0) {
timeInstance = (SimpleDateFormat)DateFormat.getTimeInstance(DateFormat.SHORT, Locale.US); // 12 hours notation
} else {
timeInstance = (SimpleDateFormat)DateFormat.getTimeInstance(DateFormat.SHORT, Locale.UK); // 24 hours notation
}
} else {
timeInstance = (SimpleDateFormat)DateFormat.getTimeInstance(DateFormat.SHORT);
}
JSpinner.DateEditor timeEditor = new JSpinner.DateEditor(this.timeSpinner, timeInstance.toPattern());
this.timeSpinner.setEditor(timeEditor);
SwingTools.addAutoSelectionOnFocusGain(timeEditor.getTextField());
final PropertyChangeListener timeChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
Date date = new Date(Camera.convertTimeToTimeZone(controller.getTime(), TimeZone.getDefault().getID()));
dateSpinnerModel.setValue(date);
timeSpinnerModel.setValue(date);
}
};
controller.addPropertyChangeListener(VideoController.Property.TIME, timeChangeListener);
final ChangeListener dateTimeChangeListener = new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
controller.removePropertyChangeListener(VideoController.Property.TIME, timeChangeListener);
// Merge date and time
GregorianCalendar dateCalendar = new GregorianCalendar();
dateCalendar.setTime((Date)dateSpinnerModel.getValue());
GregorianCalendar timeCalendar = new GregorianCalendar();
timeCalendar.setTime((Date)timeSpinnerModel.getValue());
Calendar utcCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
utcCalendar.set(GregorianCalendar.YEAR, dateCalendar.get(GregorianCalendar.YEAR));
utcCalendar.set(GregorianCalendar.MONTH, dateCalendar.get(GregorianCalendar.MONTH));
utcCalendar.set(GregorianCalendar.DAY_OF_MONTH, dateCalendar.get(GregorianCalendar.DAY_OF_MONTH));
utcCalendar.set(GregorianCalendar.HOUR_OF_DAY, timeCalendar.get(GregorianCalendar.HOUR_OF_DAY));
utcCalendar.set(GregorianCalendar.MINUTE, timeCalendar.get(GregorianCalendar.MINUTE));
utcCalendar.set(GregorianCalendar.SECOND, timeCalendar.get(GregorianCalendar.SECOND));
controller.setTime(utcCalendar.getTimeInMillis());
controller.addPropertyChangeListener(VideoController.Property.TIME, timeChangeListener);
}
};
dateSpinnerModel.addChangeListener(dateTimeChangeListener);
timeSpinnerModel.addChangeListener(dateTimeChangeListener);
this.dayNightLabel = new JLabel();
final ImageIcon dayIcon = new ImageIcon(PhotoPanel.class.getResource("resources/day.png"));
final ImageIcon nightIcon = new ImageIcon(PhotoPanel.class.getResource("resources/night.png"));
PropertyChangeListener dayNightListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
if (home.getCompass().getSunElevation(
Camera.convertTimeToTimeZone(controller.getTime(), home.getCompass().getTimeZone())) > 0) {
dayNightLabel.setIcon(dayIcon);
} else {
dayNightLabel.setIcon(nightIcon);
}
}
};
controller.addPropertyChangeListener(VideoController.Property.TIME, dayNightListener);
home.getCompass().addPropertyChangeListener(dayNightListener);
dayNightListener.propertyChange(null);
this.ceilingLightEnabledCheckBox = new JCheckBox();
this.ceilingLightEnabledCheckBox.setSelected(controller.getCeilingLightColor() > 0);
controller.addPropertyChangeListener(VideoController.Property.CEILING_LIGHT_COLOR,
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
ceilingLightEnabledCheckBox.setSelected(controller.getCeilingLightColor() > 0);
}
});
this.ceilingLightEnabledCheckBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent ev) {
controller.setCeilingLightColor(ceilingLightEnabledCheckBox.isSelected() ? 0xD0D0D0 : 0);
}
});
this.createButton = new JButton(actionMap.get(ActionType.START_VIDEO_CREATION));
this.saveButton = new JButton(actionMap.get(ActionType.SAVE_VIDEO));
this.closeButton = new JButton(actionMap.get(ActionType.CLOSE));
setComponentTexts(preferences);
updatePlaybackTimer();
this.videoFormatComboBox.setSelectedItem(new VideoFormat(VideoFormat.JPEG,
new Dimension(controller.getWidth(), controller.getHeight()), Format.NOT_SPECIFIED, Format.byteArray, controller.getFrameRate()));
}
/**
* Returns the slider value matching a given x.
*/
private float getSliderValueAt(JSlider qualitySlider, int x, UserPreferences preferences) {
int fastLabelOffset = OperatingSystem.isLinux()
? 0
: new JLabel(SwingTools.getLocalizedLabelText(preferences,
VideoPanel.class, "fastLabel.text")).getPreferredSize().width / 2;
int bestLabelOffset = OperatingSystem.isLinux()
? 0
: new JLabel(SwingTools.getLocalizedLabelText(preferences,
VideoPanel.class, "bestLabel.text")).getPreferredSize().width / 2;
int sliderWidth = qualitySlider.getWidth() - fastLabelOffset - bestLabelOffset;
return qualitySlider.getMinimum()
+ (float)(x - (qualitySlider.getComponentOrientation().isLeftToRight()
? fastLabelOffset
: bestLabelOffset))
/ sliderWidth * (qualitySlider.getMaximum() - qualitySlider.getMinimum());
}
/**
* Sets the texts of the components.
*/
private void setComponentTexts(UserPreferences preferences) {
this.tipLabel.setText(preferences.getLocalizedString(VideoPanel.class, "tipLabel.text"));
this.videoFormatLabel.setText(preferences.getLocalizedString(
VideoPanel.class, "videoFormatLabel.text"));
this.videoFormatComboBoxFormat = preferences.getLocalizedString(
VideoPanel.class, "videoFormatComboBox.format");
this.qualityLabel.setText(SwingTools.getLocalizedLabelText(preferences,
VideoPanel.class, "qualityLabel.text"));
this.dateLabel.setText(SwingTools.getLocalizedLabelText(preferences,
VideoPanel.class, "dateLabel.text"));
this.timeLabel.setText(SwingTools.getLocalizedLabelText(preferences,
VideoPanel.class, "timeLabel.text"));
this.ceilingLightEnabledCheckBox.setText(SwingTools.getLocalizedLabelText(preferences,
VideoPanel.class, "ceilingLightEnabledCheckBox.text"));
JLabel fastLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
VideoPanel.class, "fastLabel.text"));
if (!Component3DManager.getInstance().isOffScreenImageSupported()) {
fastLabel.setEnabled(false);
}
JLabel bestLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
VideoPanel.class, "bestLabel.text"));
Dictionary<Integer,JComponent> qualitySliderLabelTable = new Hashtable<Integer,JComponent>();
qualitySliderLabelTable.put(this.qualitySlider.getMinimum(), fastLabel);
qualitySliderLabelTable.put(this.qualitySlider.getMaximum(), bestLabel);
this.qualitySlider.setLabelTable(qualitySliderLabelTable);
this.dialogTitle = preferences.getLocalizedString(VideoPanel.class, "createVideo.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.videoFormatLabel.setDisplayedMnemonic(
KeyStroke.getKeyStroke(preferences.getLocalizedString(
VideoPanel.class, "videoFormatLabel.mnemonic")).getKeyCode());
this.videoFormatLabel.setLabelFor(this.videoFormatComboBox);
this.qualityLabel.setDisplayedMnemonic(
KeyStroke.getKeyStroke(preferences.getLocalizedString(
VideoPanel.class, "qualityLabel.mnemonic")).getKeyCode());
this.qualityLabel.setLabelFor(this.qualitySlider);
this.dateLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
VideoPanel.class, "dateLabel.mnemonic")).getKeyCode());
this.dateLabel.setLabelFor(this.dateSpinner);
this.timeLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
VideoPanel.class, "timeLabel.mnemonic")).getKeyCode());
this.timeLabel.setLabelFor(this.timeSpinner);
this.ceilingLightEnabledCheckBox.setMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
VideoPanel.class, "ceilingLightEnabledCheckBox.mnemonic")).getKeyCode());
}
}
/**
* 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<VideoPanel> videoPanel;
public LanguageChangeListener(VideoPanel videoPanel) {
this.videoPanel = new WeakReference<VideoPanel>(videoPanel);
}
public void propertyChange(PropertyChangeEvent ev) {
// If video panel was garbage collected, remove this listener from preferences
VideoPanel videoPanel = this.videoPanel.get();
UserPreferences preferences = (UserPreferences)ev.getSource();
if (videoPanel == null) {
preferences.removePropertyChangeListener(UserPreferences.Property.LANGUAGE, this);
} else {
videoPanel.setComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
videoPanel.setComponentTexts(preferences);
videoPanel.setMnemonics(preferences);
}
}
}
/**
* Layouts panel components in panel with their labels.
*/
private void layoutComponents() {
int labelAlignment = OperatingSystem.isMacOSX()
? GridBagConstraints.LINE_END
: GridBagConstraints.LINE_START;
// 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 BorderLayout(5, 2));
progressPanel.add(this.progressBar, BorderLayout.NORTH);
progressPanel.add(this.progressLabel);
this.statusPanel.add(progressPanel, PROGRESS_CARD);
// First row
add(this.planComponent, new GridBagConstraints(
0, 0, 4, 1, 1, 1, labelAlignment,
GridBagConstraints.BOTH, new Insets(0, 0, 5, 0), 0, 0));
// Second row
add(this.videoToolBar, new GridBagConstraints(
0, 1, 4, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
// Third row
add(this.statusPanel, new GridBagConstraints(
0, 2, 4, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.NONE, new Insets(0, 0, 10, 0), 0, 0));
// Fourth row
// Add a dummy label at left and right
add(new JLabel(), new GridBagConstraints(
0, 3, 1, 6, 0.5f, 0, GridBagConstraints.CENTER,
GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
add(new JLabel(), new GridBagConstraints(
3, 3, 1, 6, 0.5f, 0, GridBagConstraints.CENTER,
GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
Insets labelInsets = new Insets(0, 0, 0, 5);
add(this.videoFormatLabel, new GridBagConstraints(
1, 3, 1, 1, 0, 0, labelAlignment,
GridBagConstraints.NONE, labelInsets, 0, 0));
Insets componentInsets = new Insets(0, 0, 0, 10);
add(this.videoFormatComboBox, new GridBagConstraints(
2, 3, 1, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, componentInsets, -50, 0));
// Fifth row
add(this.qualityLabel, new GridBagConstraints(
1, 4, 1, 1, 0, 0, labelAlignment,
GridBagConstraints.NONE, new Insets(0, 0, 2, 5), 0, 0));
add(this.qualitySlider, new GridBagConstraints(
2, 4, 1, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 2, 0), 0, 0));
// Sixth row
add(this.advancedComponentsSeparator, new GridBagConstraints(
1, 6, 4, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.HORIZONTAL, new Insets(3, 0, 3, 0), 0, 0));
// Seventh row
JPanel advancedPanel = new JPanel(new GridBagLayout());
advancedPanel.add(this.dateLabel, new GridBagConstraints(
1, 7, 1, 1, 0, 0, labelAlignment,
GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
advancedPanel.add(this.dateSpinner, new GridBagConstraints(
2, 7, 1, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 10), 1, 0));
advancedPanel.add(this.timeLabel, new GridBagConstraints(
3, 7, 1, 1, 0, 0, labelAlignment,
GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
advancedPanel.add(this.timeSpinner, new GridBagConstraints(
4, 7, 1, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 5), 0, 0));
advancedPanel.add(this.dayNightLabel, new GridBagConstraints(
5, 7, 1, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
// Last row
advancedPanel.add(this.ceilingLightEnabledCheckBox, new GridBagConstraints(
1, 8, 5, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
add(advancedPanel, new GridBagConstraints(
0, 7, 4, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
}
private void updateAdvancedComponents() {
Component root = SwingUtilities.getRoot(this);
if (root != null) {
boolean highQuality = controller.getQuality() >= 2;
boolean advancedComponentsVisible = this.advancedComponentsSeparator.isVisible();
if (advancedComponentsVisible != highQuality) {
int componentsHeight = this.advancedComponentsSeparator.getPreferredSize().height + 6
+ this.dateSpinner.getPreferredSize().height + 5
+ this.ceilingLightEnabledCheckBox.getPreferredSize().height;
this.advancedComponentsSeparator.setVisible(highQuality);
this.dateLabel.setVisible(highQuality);
this.dateSpinner.setVisible(highQuality);
this.timeLabel.setVisible(highQuality);
this.timeSpinner.setVisible(highQuality);
this.dayNightLabel.setVisible(highQuality);
this.ceilingLightEnabledCheckBox.setVisible(highQuality);
root.setSize(root.getWidth(),
root.getHeight() + (advancedComponentsVisible ? -componentsHeight : componentsHeight));
}
}
}
/**
* Displays this panel in a non modal dialog.
*/
public void displayView(View parentView) {
if (currentVideoPanel == this) {
SwingUtilities.getWindowAncestor(VideoPanel.this).toFront();
} else {
if (currentVideoPanel != null) {
currentVideoPanel.close();
}
final JOptionPane optionPane = new JOptionPane(this,
JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION,
null, new Object [] {this.createButton, this.saveButton, this.closeButton}, this.createButton);
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);
if (homeRoot != null) {
// Restore location if it exists
Integer x = (Integer)this.home.getVisualProperty(VIDEO_DIALOG_X_VISUAL_PROPERTY);
Integer y = (Integer)this.home.getVisualProperty(VIDEO_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) {
dialog.setLocation(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
dialog.setLocationByPlatform(false);
dialog.setLocation(Math.min(windowRightBorder + 5, screenRightBorder - dialogWidth),
Math.max(Math.min(homeRoot.getY() + dialog.getInsets().top,
screenSize.height - dialogHeight - screenInsets.bottom), screenInsets.top));
} else {
dialog.setLocationByPlatform(true);
}
} else {
dialog.setLocationByPlatform(true);
}
dialog.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent ev) {
stopVideoCreation(false);
if (playbackTimer != null) {
pausePlayback();
}
if (videoFile != null) {
videoFile.delete();
}
currentVideoPanel = null;
}
});
updateAdvancedComponents();
ToolTipManager.sharedInstance().registerComponent(this.qualitySlider);
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(VIDEO_DIALOG_X_VISUAL_PROPERTY, dialog.getX());
controller.setVisualProperty(VIDEO_DIALOG_Y_VISUAL_PROPERTY, dialog.getY());
}
});
dialog.setVisible(true);
currentVideoPanel = this;
}
}
/**
* Records the location and the angles of the current camera.
*/
private void recordCameraLocation() {
List<Camera> cameraPath = this.controller.getCameraPath();
Camera camera = this.home.getCamera();
Camera lastCamera = null;
if (cameraPath.size() > 0) {
lastCamera = cameraPath.get(cameraPath.size() - 1);
}
if (lastCamera == null
|| !compareCameraLocation(lastCamera, camera)) {
// Record only new locations
cameraPath = new ArrayList<Camera>(cameraPath);
Camera recordedCamera = camera.clone();
recordedCamera.setLens(Camera.Lens.NORMAL);
recordedCamera.setTime(this.controller.getTime());
cameraPath.add(recordedCamera);
this.controller.setCameraPath(cameraPath);
}
}
/**
* Returns <code>true</code> if the given cameras are at the same location.
*/
private boolean compareCameraLocation(Camera camera1, Camera camera2) {
return camera1.getX() == camera2.getX()
&& camera1.getY() == camera2.getY()
&& camera1.getZ() == camera2.getZ()
&& camera1.getYaw() == camera2.getYaw()
&& camera1.getPitch() == camera2.getPitch()
&& camera1.getFieldOfView() == camera2.getFieldOfView()
&& camera1.getTime() == camera2.getTime();
}
/**
* Updates the timer used for playback.
*/
private void updatePlaybackTimer() {
final List<Camera> cameraPath = this.controller.getCameraPath();
final ActionMap actionMap = getActionMap();
boolean playable = cameraPath.size() > 1;
if (playable) {
Camera [] videoFramesPath = getVideoFramesPath(12);
// Find current camera location
Camera homeCamera = home.getCamera();
int index = videoFramesPath.length;
while (--index > 0
&& !compareCameraLocation(videoFramesPath [index], homeCamera)) {
}
// Prefer last location
if (index < 0 || index == videoFramesPath.length - 1) {
index = videoFramesPath.length;
}
this.cameraPathIterator = Arrays.asList(videoFramesPath).listIterator(index);
ActionListener playbackAction = new ActionListener() {
public void actionPerformed(ActionEvent ev) {
if ("backward".equals(ev.getActionCommand())) {
if (cameraPathIterator.hasPrevious()) {
Camera camera = cameraPathIterator.previous();
home.getCamera().setCamera(camera);
controller.setTime(camera.getTime());
} else {
pausePlayback();
}
} else {
if (cameraPathIterator.hasNext()) {
Camera camera = cameraPathIterator.next();
home.getCamera().setCamera(camera);
controller.setTime(camera.getTime());
} else {
pausePlayback();
}
}
boolean pathEditable = videoCreationExecutor == null && !((Timer)ev.getSource()).isRunning();
actionMap.get(ActionType.RECORD).setEnabled(pathEditable);
actionMap.get(ActionType.DELETE_CAMERA_PATH).setEnabled(pathEditable && cameraPath.size() > 0);
actionMap.get(ActionType.DELETE_LAST_RECORD).setEnabled(pathEditable && cameraPath.size() > 0);
actionMap.get(ActionType.SEEK_BACKWARD).setEnabled(cameraPathIterator.hasPrevious());
actionMap.get(ActionType.SKIP_BACKWARD).setEnabled(cameraPathIterator.hasPrevious());
actionMap.get(ActionType.SEEK_FORWARD).setEnabled(cameraPathIterator.hasNext());
actionMap.get(ActionType.SKIP_FORWARD).setEnabled(cameraPathIterator.hasNext());
}
};
if (this.playbackTimer != null) {
this.playbackTimer.stop();
}
this.playbackTimer = new Timer(1000 / 12, playbackAction);
this.playbackTimer.setInitialDelay(0);
this.playbackTimer.setCoalesce(false);
}
actionMap.get(ActionType.PLAYBACK).setEnabled(playable);
actionMap.get(ActionType.RECORD).setEnabled(this.videoCreationExecutor == null);
boolean emptyCameraPath = cameraPath.isEmpty();
actionMap.get(ActionType.DELETE_CAMERA_PATH).setEnabled(this.videoCreationExecutor == null && !emptyCameraPath);
actionMap.get(ActionType.DELETE_LAST_RECORD).setEnabled(this.videoCreationExecutor == null && !emptyCameraPath);
actionMap.get(ActionType.SEEK_BACKWARD).setEnabled(playable && this.cameraPathIterator.hasPrevious());
actionMap.get(ActionType.SKIP_BACKWARD).setEnabled(playable && this.cameraPathIterator.hasPrevious());
actionMap.get(ActionType.SEEK_FORWARD).setEnabled(playable && this.cameraPathIterator.hasNext());
actionMap.get(ActionType.SKIP_FORWARD).setEnabled(playable && this.cameraPathIterator.hasNext());
actionMap.get(ActionType.START_VIDEO_CREATION).setEnabled(playable);
}
/**
* Deletes the last recorded camera location.
*/
private void deleteLastRecordedCameraLocation() {
List<Camera> cameraPath = new ArrayList<Camera>(this.controller.getCameraPath());
cameraPath.remove(cameraPath.size() - 1);
this.controller.setCameraPath(cameraPath);
}
/**
* Deletes the recorded camera path.
*/
private void deleteCameraPath() {
List<Camera> cameraPath = Collections.emptyList();
this.controller.setCameraPath(cameraPath );
}
/**
* Plays back the camera locations.
*/
private void playback() {
if (!this.cameraPathIterator.hasNext()) {
skipBackward();
}
this.playbackTimer.start();
this.playbackPauseButton.setAction(getActionMap().get(ActionType.PAUSE));
}
/**
* Pauses play back.
*/
private void pausePlayback() {
this.playbackTimer.stop();
this.playbackPauseButton.setAction(getActionMap().get(ActionType.PLAYBACK));
getActionMap().get(ActionType.RECORD).setEnabled(this.videoCreationExecutor == null);
boolean emptyCameraPath = this.controller.getCameraPath().isEmpty();
getActionMap().get(ActionType.DELETE_CAMERA_PATH).setEnabled(this.videoCreationExecutor == null && !emptyCameraPath);
getActionMap().get(ActionType.DELETE_LAST_RECORD).setEnabled(this.videoCreationExecutor == null && !emptyCameraPath);
}
/**
* Moves quickly camera 10 steps backward.
*/
private void seekBackward() {
for (int i = 0; i < 10 && this.cameraPathIterator.hasPrevious(); i++) {
this.playbackTimer.getActionListeners() [0].actionPerformed(
new ActionEvent(this.playbackTimer, 0, "backward", System.currentTimeMillis(), 0));
}
}
/**
* Moves quickly camera 10 steps forward.
*/
private void seekForward() {
for (int i = 0; i < 10 && this.cameraPathIterator.hasNext(); i++) {
this.playbackTimer.getActionListeners() [0].actionPerformed(
new ActionEvent(this.playbackTimer, 0, "forward", System.currentTimeMillis(), 0));
}
}
/**
* Moves camera to animation start and restarts animation if it was running.
*/
private void skipBackward() {
while (this.cameraPathIterator.hasPrevious()) {
seekBackward();
}
}
/**
* Moves camera to animation end and stops animation.
*/
private void skipForward() {
while (this.cameraPathIterator.hasNext()) {
seekForward();
}
}
/**
* Returns the camera path that should be used to create each frame of an animation.
*/
private Camera [] getVideoFramesPath(int frameRate) {
List<Camera> videoFramesPath = new ArrayList<Camera>();
final float moveDistancePerFrame = 240000f / 3600 / frameRate; // 3 cm/frame = 1800 m / 3600 s / 25 frame/s = 2.4 km/h
final float moveAnglePerFrame = (float)(Math.PI / 180 * 30 / frameRate);
final float elapsedTimePerFrame = 345600 / frameRate * 25; // 250 frame/day at 25 frame/second
List<Camera> cameraPath = this.controller.getCameraPath();
Camera camera = cameraPath.get(0);
float x = camera.getX();
float y = camera.getY();
float z = camera.getZ();
float yaw = camera.getYaw();
float pitch = camera.getPitch();
float fieldOfView = camera.getFieldOfView();
long time = camera.getTime();
videoFramesPath.add(camera.clone());
for (int i = 1; i < cameraPath.size(); i++) {
camera = cameraPath.get(i);
float newX = camera.getX();
float newY = camera.getY();
float newZ = camera.getZ();
float newYaw = camera.getYaw();
float newPitch = camera.getPitch();
float newFieldOfView = camera.getFieldOfView();
long newTime = camera.getTime();
float distance = new Point3f(x, y, z).distance(new Point3f(newX, newY, newZ));
float moveCount = distance / moveDistancePerFrame;
float yawAngleCount = Math.abs(newYaw - yaw) / moveAnglePerFrame;
float pitchAngleCount = Math.abs(newPitch - pitch) / moveAnglePerFrame;
float fieldOfViewAngleCount = Math.abs(newFieldOfView - fieldOfView) / moveAnglePerFrame;
float timeCount = Math.abs(newTime - time) / elapsedTimePerFrame;
int frameCount = (int)Math.max(moveCount, Math.max(yawAngleCount,
Math.max(pitchAngleCount, Math.max(fieldOfViewAngleCount, timeCount))));
float deltaX = (newX - x) / frameCount;
float deltaY = (newY - y) / frameCount;
float deltaZ = (newZ - z) / frameCount;
float deltaYawAngle = (newYaw - yaw) / frameCount;
float deltaPitchAngle = (newPitch - pitch) / frameCount;
float deltaFieldOfViewAngle = (newFieldOfView - fieldOfView) / frameCount;
long deltaTime = Math.round(((double)newTime - time) / frameCount);
for (int j = 1; j <= frameCount; j++) {
videoFramesPath.add(new Camera(
x + deltaX * j, y + deltaY * j, z + deltaZ * j,
yaw + deltaYawAngle * j, pitch + deltaPitchAngle * j,
fieldOfView + deltaFieldOfViewAngle * j,
time + deltaTime * j,
Camera.Lens.NORMAL));
}
x = newX;
y = newY;
z = newZ;
yaw = newYaw;
pitch = newPitch;
fieldOfView = newFieldOfView;
time = newTime;
}
return videoFramesPath.toArray(new Camera [videoFramesPath.size()]);
}
/**
* Creates the video image depending on the quality requested by the user.
*/
private void startVideoCreation() {
ActionMap actionMap = getActionMap();
actionMap.get(ActionType.SAVE_VIDEO).setEnabled(false);
this.createButton.setAction(getActionMap().get(ActionType.STOP_VIDEO_CREATION));
actionMap.get(ActionType.RECORD).setEnabled(false);
actionMap.get(ActionType.DELETE_CAMERA_PATH).setEnabled(false);
actionMap.get(ActionType.DELETE_LAST_RECORD).setEnabled(false);
getRootPane().setDefaultButton(this.createButton);
this.videoFormatComboBox.setEnabled(false);
this.qualitySlider.setEnabled(false);
this.dateSpinner.setEnabled(false);
this.timeSpinner.setEnabled(false);
this.ceilingLightEnabledCheckBox.setEnabled(false);
this.statusLayout.show(this.statusPanel, PROGRESS_CARD);
this.progressBar.setIndeterminate(true);
this.progressLabel.setText("");
// Compute video in an other executor thread
// Use a clone of home because the user can modify home during video computation
final Home home = this.home.clone();
this.videoCreationExecutor = Executors.newSingleThreadExecutor();
this.videoCreationExecutor.execute(new Runnable() {
public void run() {
computeVideo(home);
}
});
}
/**
* Computes the video of the given home.
* Caution : this method must be thread safe because it's called from an executor.
*/
private void computeVideo(Home home) {
this.videoCreationStartTime = System.currentTimeMillis();
int frameRate = this.controller.getFrameRate();
int quality = this.controller.getQuality();
int width = this.controller.getWidth();
int height = this.controller.getHeight();
final Camera [] videoFramesPath = getVideoFramesPath(frameRate);
// Set initial camera location because its type may change rendering setting
home.setCamera(videoFramesPath [0]);
final BoundedRangeModel progressModel = this.progressBar.getModel();
EventQueue.invokeLater(new Runnable() {
public void run() {
progressModel.setMinimum(0);
progressModel.setMaximum(videoFramesPath.length);
progressModel.setValue(0);
}
});
FrameGenerator frameGenerator = null;
// Delete previous file if it exists
if (this.videoFile != null) {
this.videoFile.delete();
this.videoFile = null;
}
File file = null;
try {
file = OperatingSystem.createTemporaryFile("video", ".mov");
if (quality >= 2) {
frameGenerator = new PhotoImageGenerator(home, width, height, quality == 2
? PhotoRenderer.Quality.LOW
: PhotoRenderer.Quality.HIGH);
} else {
frameGenerator = new Image3DGenerator(home, width, height, quality == 1);
}
if (!Thread.currentThread().isInterrupted()) {
ImageDataSource sourceStream = new ImageDataSource((VideoFormat)this.videoFormatComboBox.getSelectedItem(),
frameGenerator, videoFramesPath, progressModel);
new JPEGImagesToVideo().createVideoFile(width, height, frameRate, sourceStream, file);
}
} catch (InterruptedIOException ex) {
if (file != null) {
file.delete();
file = null;
}
} catch (IOException ex) {
showError("createVideoError.message", ex.getMessage());
file = null;
} catch (OutOfMemoryError ex) {
showError("createVideoError.message",
preferences.getLocalizedString(VideoPanel.class, "outOfMemory.message"));
file = null;
} finally {
if (videoCreationExecutor != null) {
this.videoFile = file;
file.deleteOnExit();
} else {
this.videoFile = file;
if (file != null) {
file.delete();
}
}
EventQueue.invokeLater(new Runnable() {
public void run() {
ActionMap actionMap = getActionMap();
actionMap.get(ActionType.SAVE_VIDEO).setEnabled(videoFile != null);
createButton.setAction(actionMap.get(ActionType.START_VIDEO_CREATION));
actionMap.get(ActionType.RECORD).setEnabled(true);
actionMap.get(ActionType.DELETE_CAMERA_PATH).setEnabled(true);
actionMap.get(ActionType.DELETE_LAST_RECORD).setEnabled(true);
if (videoFile != null) {
getRootPane().setDefaultButton(saveButton);
}
videoFormatComboBox.setEnabled(true);
qualitySlider.setEnabled(true);
dateSpinner.setEnabled(true);
timeSpinner.setEnabled(true);
ceilingLightEnabledCheckBox.setEnabled(true);
statusLayout.show(statusPanel, TIP_CARD);
videoCreationExecutor = null;
}
});
}
}
/**
* Shows a message error dialog.
*/
private void showError(final String messageKey,
final String messageDetail) {
EventQueue.invokeLater(new Runnable() {
public void run() {
String messageFormat = preferences.getLocalizedString(VideoPanel.class, messageKey);
JOptionPane.showMessageDialog(SwingUtilities.getRootPane(VideoPanel.this), String.format(messageFormat, messageDetail),
preferences.getLocalizedString(VideoPanel.class, "videoError.title"), JOptionPane.ERROR_MESSAGE);
}
});
}
/**
* Stops video creation.
*/
private void stopVideoCreation(boolean confirmStop) {
if (this.videoCreationExecutor != null
// Confirm the stop if a rendering has been running for more than 30 s
&& (!confirmStop
|| System.currentTimeMillis() - this.videoCreationStartTime < MINIMUM_DELAY_BEFORE_DISCARDING_WITHOUT_WARNING
|| JOptionPane.showConfirmDialog(getRootPane(),
this.preferences.getLocalizedString(VideoPanel.class, "confirmStopCreation.message"),
this.preferences.getLocalizedString(VideoPanel.class, "confirmStopCreation.title"),
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION)) {
if (this.videoCreationExecutor != null) { // Check a second time in case rendering stopped meanwhile
// Interrupt executor thread
this.videoCreationExecutor.shutdownNow();
this.videoCreationExecutor = null;
this.createButton.setAction(getActionMap().get(ActionType.START_VIDEO_CREATION));
}
}
}
/**
* Saves the created video.
*/
private void saveVideo() {
final String movFileName = this.controller.getContentManager().showSaveDialog(this,
this.preferences.getLocalizedString(VideoPanel.class, "saveVideoDialog.title"),
ContentManager.ContentType.MOV, this.home.getName());
if (movFileName != null) {
final Component rootPane = SwingUtilities.getRoot(this);
final Cursor defaultCursor = rootPane.getCursor();
rootPane.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
// Disable panel actions
ActionMap actionMap = getActionMap();
final boolean [] actionEnabledStates = new boolean [ActionType.values().length];
for (ActionType action : ActionType.values()) {
actionEnabledStates [action.ordinal()] = actionMap.get(action).isEnabled();
actionMap.get(action).setEnabled(false);
}
Executors.newSingleThreadExecutor().execute(new Runnable() {
public void run() {
OutputStream out = null;
InputStream in = null;
IOException exception = null;
try {
// Copy temporary file to home file
// Overwriting home file will ensure that its rights are kept
out = new FileOutputStream(movFileName);
byte [] buffer = new byte [8192];
in = new FileInputStream(videoFile);
int size;
while ((size = in.read(buffer)) != -1 && isDisplayable()) {
out.write(buffer, 0, size);
}
} catch (IOException ex) {
exception = ex;
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException ex) {
if (exception == null) {
exception = ex;
}
}
try {
if (in != null) {
in.close();
}
} catch (IOException ex) {
// Ignore close exception
}
// Delete saved file in case of error or if panel was closed meanwhile
if (exception != null || !isDisplayable()) {
new File(movFileName).delete();
if (!isDisplayable()) {
exception = null;
}
}
final IOException caughtException = exception;
EventQueue.invokeLater(new Runnable() {
public void run() {
// Restore action state
ActionMap actionMap = getActionMap();
for (ActionType action : ActionType.values()) {
actionMap.get(action).setEnabled(actionEnabledStates [action.ordinal()]);
}
rootPane.setCursor(defaultCursor);
if (caughtException != null) {
showError("saveVideoError.message", caughtException.getMessage());
}
}
});
}
}
});
}
}
/**
* Manages closing of this pane.
*/
private void close() {
Window window = SwingUtilities.getWindowAncestor(this);
if (window.isDisplayable()) {
ToolTipManager.sharedInstance().unregisterComponent(this.qualitySlider);
window.dispose();
}
}
/**
* Returns the aspect ration of the given video format.
*/
private AspectRatio getAspectRatio(VideoFormat videoFormat) {
Dimension videoSize = videoFormat.getSize();
return Math.abs((float)videoSize.width / videoSize.height - 4f / 3) < 0.001f
? AspectRatio.RATIO_4_3
: AspectRatio.RATIO_16_9;
}
/**
* A data source able to create JPEG buffers on the fly from camera path
* and turn that into a stream of JMF buffers.
*/
private static class ImageDataSource extends PullBufferDataSource {
private ImageSourceStream stream;
public ImageDataSource(VideoFormat format,
FrameGenerator frameGenerator,
Camera [] framesPath,
BoundedRangeModel progressModel) {
this.stream = new ImageSourceStream(format, frameGenerator, framesPath, progressModel);
}
@Override
public void setLocator(MediaLocator source) {
}
@Override
public MediaLocator getLocator() {
return null;
}
/**
* Returns RAW since buffers of video frames are sent without a container format.
*/
@Override
public String getContentType() {
return ContentDescriptor.RAW;
}
@Override
public void connect() {
}
@Override
public void disconnect() {
}
@Override
public void start() {
}
@Override
public void stop() {
}
public PullBufferStream [] getStreams() {
return new PullBufferStream [] {this.stream};
}
/**
* Not necessary to compute the duration.
*/
@Override
public Time getDuration() {
return DURATION_UNKNOWN;
}
@Override
public Object [] getControls() {
return new Object [0];
}
@Override
public Object getControl(String type) {
return null;
}
}
/**
* A source of video images.
*/
private static class ImageSourceStream implements PullBufferStream {
private final FrameGenerator frameGenerator;
private final Camera [] framesPath;
private final BoundedRangeModel progressModel;
private final javax.media.format.VideoFormat format;
private int imageIndex;
private boolean stopped;
public ImageSourceStream(VideoFormat format,
FrameGenerator frameGenerator,
Camera [] framesPath,
final BoundedRangeModel progressModel) {
this.frameGenerator = frameGenerator;
this.framesPath = framesPath;
this.progressModel = progressModel;
this.format = format;
}
/**
* Return <code>false</code> because source stream doesn't
* need to block assuming data can be created on demand.
*/
public boolean willReadBlock() {
return false;
}
/**
* This is called from the Processor to read a frame worth of video data.
*/
public void read(Buffer buffer) throws IOException {
buffer.setOffset(0);
// Check if we've finished all the frames
if (endOfStream()) {
buffer.setEOM(true);
buffer.setLength(0);
} else {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BufferedImage frame = this.frameGenerator.renderImageAt(this.framesPath [this.imageIndex],
this.imageIndex == this.framesPath.length - 1);
ImageIO.write(frame, "JPEG", outputStream);
byte [] data = outputStream.toByteArray();
buffer.setData(data);
buffer.setLength(data.length);
buffer.setFormat(this.format);
buffer.setFlags(buffer.getFlags() | Buffer.FLAG_KEY_FRAME);
final int progressionValue = this.imageIndex++;
EventQueue.invokeLater(new Runnable() {
public void run() {
progressModel.setValue(progressionValue);
}
});
}
}
/**
* Return the format of each video frame. That will be JPEG.
*/
public Format getFormat() {
return this.format;
}
public ContentDescriptor getContentDescriptor() {
return new ContentDescriptor(ContentDescriptor.RAW);
}
public long getContentLength() {
return 0;
}
public boolean endOfStream() {
return this.stopped || this.imageIndex == this.framesPath.length;
}
public Object [] getControls() {
return new Object [0];
}
public Object getControl(String type) {
return null;
}
}
/**
* An object able to generate a frame of a video at a camera location.
*/
private static abstract class FrameGenerator {
private Thread launchingThread;
protected FrameGenerator() {
this.launchingThread = Thread.currentThread();
}
public abstract BufferedImage renderImageAt(Camera frameCamera, boolean last) throws IOException;
protected void checkLaunchingThreadIsntInterrupted() throws InterruptedIOException {
if (this.launchingThread.isInterrupted()) {
throw new InterruptedIOException("Lauching thread interrupted");
}
}
}
/**
* A frame generator using photo renderer.
*/
private static class PhotoImageGenerator extends FrameGenerator {
private PhotoRenderer renderer;
private BufferedImage image;
public PhotoImageGenerator(Home home, int width, int height,
PhotoRenderer.Quality quality) throws IOException {
this.renderer = new PhotoRenderer(home, quality);
this.image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
}
public BufferedImage renderImageAt(Camera frameCamera, boolean last) throws IOException {
try {
checkLaunchingThreadIsntInterrupted();
this.renderer.render(this.image, frameCamera, null);
checkLaunchingThreadIsntInterrupted();
return image;
} catch(InterruptedIOException ex) {
this.renderer = null;
throw ex;
} finally {
if (last) {
this.renderer = null;
}
}
}
}
/**
* A frame generator using 3D offscreen images.
*/
private static class Image3DGenerator extends FrameGenerator {
private final Home home;
private HomeComponent3D homeComponent3D;
private BufferedImage image;
public Image3DGenerator(Home home, int width, int height,
boolean displayShadowOnFloor) {
this.home = home;
this.homeComponent3D = new HomeComponent3D(home, null, displayShadowOnFloor);
this.homeComponent3D.startOffscreenImagesCreation();
this.image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
}
public BufferedImage renderImageAt(Camera frameCamera, boolean last) throws IOException {
try {
checkLaunchingThreadIsntInterrupted();
// Replace home camera with frameCamera to avoid animation interpolator in 3D component
this.home.setCamera(frameCamera);
// Get a twice bigger offscreen image for better quality
// (antialiasing isn't always available for offscreen canvas)
BufferedImage offScreenImage = this.homeComponent3D.getOffScreenImage(
2 * this.image.getWidth(), 2 * this.image.getHeight());
checkLaunchingThreadIsntInterrupted();
Graphics graphics = this.image.getGraphics();
graphics.drawImage(offScreenImage.getScaledInstance(
this.image.getWidth(), this.image.getHeight(), Image.SCALE_SMOOTH), 0, 0, null);
graphics.dispose();
checkLaunchingThreadIsntInterrupted();
return this.image;
} catch(InterruptedIOException ex) {
this.homeComponent3D.endOffscreenImagesCreation();
throw ex;
} finally {
if (last) {
this.homeComponent3D.endOffscreenImagesCreation();
}
}
}
}
}