/* * Open Source Physics software is free software as described near the bottom of this code file. * * For additional information and documentation on Open Source Physics please see: * <http://www.opensourcephysics.org/> */ /* * The org.opensourcephysics.media.core package defines the Open Source Physics * media framework for working with video and other media. * * Copyright (c) 2014 Douglas Brown and Wolfgang Christian. * * This 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 software 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; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA * or view the license online at http://www.gnu.org/copyleft/gpl.html * * For additional information and documentation on Open Source Physics, * please see <http://www.opensourcephysics.org/>. */ package org.opensourcephysics.media.core; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Graphics; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import javax.swing.SwingUtilities; import org.opensourcephysics.controls.OSPLog; import org.opensourcephysics.controls.XML; import org.opensourcephysics.controls.XMLControl; import org.opensourcephysics.display.Data; import org.opensourcephysics.display.Drawable; import org.opensourcephysics.display.InteractivePanel; import org.opensourcephysics.display.TextPanel; /** * This is an interactive drawing panel with a video player. * It can draw videos and other Trackable objects in either * imagespace or worldspace. When drawing in imagespace, the image * reference frame (ie the image itself) is fixed. When * drawing in worldspace, the world reference frame is fixed. * The image reference frame defines positions in pixel units * relative to the upper left corner of a video image--ie, the UL * corner of a 320 x 240 video is at (0.0, 0.0) and the LR corner * is at (320.0, 240.0). When drawing in imagespace, non-Trackable * objects are not drawn. * * @author Douglas Brown * @version 1.0 */ public class VideoPanel extends InteractivePanel implements PropertyChangeListener { // static fields protected static int defaultWidth = 640; protected static int defaultHeight = 480; // instance fields /** set to true when this panel has been changed, false when saved */ public boolean changed = false; /** default file name used for initial saveAs */ public String defaultFileName; protected VideoPlayer player; protected TextPanel mousePanel; protected TextPanel messagePanel; protected Video video = null; protected boolean playerVisible = true; protected boolean drawingInImageSpace = false; protected double imageWidth, imageHeight; // image dimensions in image units protected double xOffset, yOffset; // imagespace drawing offset in pixel units protected double imageBorder; // a fraction >= 0 protected ImageCoordSystem coords; // image <--> world transforms protected Point2D pt = new Point2D.Double(); protected File dataFile; protected Map<String, Class<? extends Filter>> filterClasses = new TreeMap<String, Class<? extends Filter>>(); // maps filter names to classes /** * Constructs a blank VideoPanel with a player. */ public VideoPanel() { this(null); } /** * Constructs a VideoPanel with a video and player. * * @param video the video to be drawn */ public VideoPanel(Video video) { setSquareAspect(true); player = new VideoPlayer(this); player.addPropertyChangeListener("videoclip", this); //$NON-NLS-1$ player.addPropertyChangeListener("stepnumber", this); //$NON-NLS-1$ player.addPropertyChangeListener("frameduration", this); //$NON-NLS-1$ add(player, BorderLayout.SOUTH); VideoClip clip = player.getVideoClip(); clip.addPropertyChangeListener("startframe", this); //$NON-NLS-1$ clip.addPropertyChangeListener("stepsize", this); //$NON-NLS-1$ clip.addPropertyChangeListener("stepcount", this); //$NON-NLS-1$ clip.addPropertyChangeListener("framecount", this); //$NON-NLS-1$ clip.addPropertyChangeListener("starttime", this); //$NON-NLS-1$ clip.addPropertyChangeListener("adjusting", this); //$NON-NLS-1$ // define mousePanel and messagePanel mousePanel = blMessageBox; messagePanel = brMessageBox; // make new CoordinateStringBuilder setCoordinateStringBuilder(new VidCartesianCoordinateStringBuilder()); // create coords and put origin at center of panel coords = new ImageCoordSystem(); setVideo(video); if((video!=null)&&(video.getImage().getWidth()>0)) { setImageWidth(video.getImage().getWidth()); setImageHeight(video.getImage().getHeight()); } else { setImageWidth(defaultWidth); setImageHeight(defaultHeight); } int w = (int) getImageWidth(); int h = (int) getImageHeight(); setPreferredSize(new Dimension(w, h+player.height)); // put origin at center of image coords.setAllOriginsXY(imageWidth/2, imageHeight/2); } /** * Sets the video. * * @param newVideo the video * @param playAllSteps true to play all steps */ public void setVideo(Video newVideo, boolean playAllSteps) { if(newVideo==video) { return; } Video prev = video; VideoClip clip = new VideoClip(newVideo); clip.setPlayAllSteps(playAllSteps); getPlayer().setVideoClip(clip); if(prev!=null) { prev.dispose(); } } /** * Sets the video. * * @param newVideo the video */ public void setVideo(Video newVideo) { setVideo(newVideo, false); } /** * Gets the video. * * @return the video */ public Video getVideo() { return video; } /** * Gets the image width in image units. * * @return the width */ public double getImageWidth() { return imageWidth; } /** * Sets the image width in image units. * * @param w the width */ public void setImageWidth(double w) { // don't allow widths smaller than the video if(video!=null) { BufferedImage vidImage = video.getImage(); if(vidImage!=null) { w = Math.max(w, vidImage.getWidth()); } } imageWidth = w; } /** * Gets the image height in image units (1.0 unit/pixel). * * @return the height */ public double getImageHeight() { return imageHeight; } /** * Sets the image height in image units (1.0 unit/pixel). * * @param h the height */ public void setImageHeight(double h) { // don't allow heights smaller than the video if(video!=null) { BufferedImage vidImage = video.getImage(); if(vidImage!=null) { h = Math.max(h, vidImage.getHeight()); } } imageHeight = h; } /** * Gets the image border. * * @return the border fraction */ public double getImageBorder() { return imageBorder; } /** * Sets the image border. * * @param borderFraction the border fraction */ public void setImageBorder(double borderFraction) { // imageBorder = Math.max(borderFraction, 0); imageBorder = borderFraction; } /** * Sets the image coordinate system used to convert between * image and world spaces. * * @param newCoords the image coordinate system */ public void setCoords(ImageCoordSystem newCoords) { if(video!=null) { video.setCoords(newCoords); } else { coords = newCoords; } } /** * Gets the current image coordinate system used for drawing. * * @return the current image coordinate system */ public ImageCoordSystem getCoords() { return coords; } /** * Sets the file in which data is saved. * * @param file the data file */ public void setDataFile(File file) { File prev = dataFile; dataFile = file; if(file!=null) { defaultFileName = XML.forwardSlash(file.getName()); } firePropertyChange("datafile", prev, dataFile); //$NON-NLS-1$ OSPLog.fine("Data file: "+file); //$NON-NLS-1$ } /** * Gets the file where data is saved. * * @return the data file */ public File getDataFile() { return dataFile; } /** * Gets the default path for the saveAs method. * * @return the relative path to the file */ public String getFilePath() { return defaultFileName; } /** * Sets the drawing space to imagespace or worldspace. * * @param imagespace <code>true</code> to draw in imagespace */ public void setDrawingInImageSpace(boolean imagespace) { drawingInImageSpace = imagespace; if(imagespace) { setAutoscaleX(false); setAutoscaleY(false); } else { setAutoscaleX(true); setAutoscaleY(true); } firePropertyChange("imagespace", null, new Boolean(imagespace)); //$NON-NLS-1$ repaint(); } /** * Returns true if this is drawing in image space rather than * world space. * * @return <code>true</code> if drawing in image space */ public boolean isDrawingInImageSpace() { return drawingInImageSpace; } /** * Gets the video player. * * @return the video player */ public VideoPlayer getPlayer() { return player; } /** * Shows or hides the video player. * * @param visible <code>true</code> to show the player */ public void setPlayerVisible(final boolean visible) { if(visible==playerVisible) { return; } Runnable setPlayerVis = new Runnable() { public void run() { playerVisible = visible; if(playerVisible) { add(player, BorderLayout.SOUTH); } else { remove(player); } repaint(); } }; SwingUtilities.invokeLater(setPlayerVis); } /** * Gets the video player visibility. * * @return <code>true</code> if the player is visible */ public boolean isPlayerVisible() { return playerVisible; } /** * Gets the current step number. * * @return the current step number */ public int getStepNumber() { return getPlayer().getStepNumber(); } /** * Gets the current frame number. * * @return the frame number */ public int getFrameNumber() { return getPlayer().getFrameNumber(); } /** * Overrides DrawingPanel getDrawables method. * * @return a list of Drawable objects */ public ArrayList<Drawable> getDrawables() { ArrayList<Drawable> list = super.getDrawables(); if(isDrawingInImageSpace()) { for(Drawable d : list) { if(!Trackable.class.isInstance(d)) { list.remove(d); } } } return list; } /** * Adds a drawable object to the drawable list. * * @param drawable the drawable object */ public synchronized void addDrawable(Drawable drawable) { if(drawable==null) { return; } if(drawable instanceof Video) { setVideo((Video) drawable); } else { super.addDrawable(drawable); } repaint(); } /** * Removes a drawable object from the drawable list. * * @param drawable the drawable object */ public synchronized void removeDrawable(Drawable drawable) { if(drawable==video) { setVideo(null); } else { super.removeDrawable(drawable); } } /** * Removes all objects of the specified class. * * @param c the class to remove */ public synchronized <T extends Drawable> void removeObjectsOfClass(Class<T> c) { if(video.getClass()==c) { setVideo(null); } else { super.removeObjectsOfClass(c); } } /** * Removes all drawable objects except the video. * To remove the video, use setVideo(null); */ public synchronized void clear() { super.clear(); // add back the video if(video!=null) { super.addDrawable(video); } } /** * Adds a video filter class to the map of available filters. * * @param filterClass the filter class to add */ public void addFilter(Class<? extends Filter> filterClass) { if(Filter.class.isAssignableFrom(filterClass)) { String name = filterClass.getName(); filterClasses.put(name, filterClass); } } /** * Removes a video filter class from the map of available filters. * * @param filterClass the filter class to remove */ public void removeFilter(Class<?> filterClass) { if(Filter.class.isAssignableFrom(filterClass)) { String name = filterClass.getName(); filterClasses.remove(name); } } /** * Clears all video filter classes from the map of available filters. */ public void clearFilters() { filterClasses.clear(); } /** * Gets the map of available video filters. * * @return the map of available video filters */ public Map<String, Class<? extends Filter>> getFilters() { return filterClasses; } /** * Returns true if mouse coordinates are displayed * * @return <code>true</code> if mouse coordinates are displayed */ public boolean isShowCoordinates() { return showCoordinates; } /** * Hides the mouse box */ public void hideMouseBox() { if(mousePanel.isVisible()) { mousePanel.setText(null); } } /** * Responds to property change events. VideoPanel listens for the following * events: "videoclip" and "stepnumber" from VideoPlayer, "coords" and "image" * from Video. * * @param e the property change event */ public void propertyChange(PropertyChangeEvent e) { String name = e.getPropertyName(); if(name.equals("size")) { // from Video //$NON-NLS-1$ Dimension dim = (Dimension) e.getNewValue(); setImageWidth(dim.width); setImageHeight(dim.height); } else if(name.equals("coords")) { // from Video //$NON-NLS-1$ // replace current coords with video's new coords coords = video.getCoords(); } else if(name.equals("image")|| //$NON-NLS-1$ name.equals("videoVisible")) { // from Video //$NON-NLS-1$ repaint(); } else if(name.equals("stepnumber")) { // from VideoPlayer //$NON-NLS-1$ repaint(); } else if(name.equals("videoclip")) { // from VideoPlayer //$NON-NLS-1$ // update property change listeners VideoClip oldClip = (VideoClip) e.getOldValue(); oldClip.removePropertyChangeListener("startframe", this); //$NON-NLS-1$ oldClip.removePropertyChangeListener("stepsize", this); //$NON-NLS-1$ oldClip.removePropertyChangeListener("stepcount", this); //$NON-NLS-1$ oldClip.removePropertyChangeListener("framecount", this); //$NON-NLS-1$ oldClip.removePropertyChangeListener("starttime", this); //$NON-NLS-1$ oldClip.removePropertyChangeListener("adjusting", this); //$NON-NLS-1$ // oldClip.removePropertyChangeListener("frameshift", this); //$NON-NLS-1$ VideoClip clip = (VideoClip) e.getNewValue(); clip.addPropertyChangeListener("startframe", this); //$NON-NLS-1$ clip.addPropertyChangeListener("stepsize", this); //$NON-NLS-1$ clip.addPropertyChangeListener("stepcount", this); //$NON-NLS-1$ clip.addPropertyChangeListener("framecount", this); //$NON-NLS-1$ clip.addPropertyChangeListener("starttime", this); //$NON-NLS-1$ clip.addPropertyChangeListener("adjusting", this); //$NON-NLS-1$ // clip.addPropertyChangeListener("frameshift", this); //$NON-NLS-1$ // replace current video with new clip's video if(video!=null) { video.removePropertyChangeListener("coords", this); //$NON-NLS-1$ video.removePropertyChangeListener("image", this); //$NON-NLS-1$ video.removePropertyChangeListener("filterChanged", this); //$NON-NLS-1$ video.removePropertyChangeListener("videoVisible", this); //$NON-NLS-1$ video.removePropertyChangeListener("size", this); //$NON-NLS-1$ super.removeDrawable(video); } video = clip.getVideo(); if(video!=null) { video.addPropertyChangeListener("coords", this); //$NON-NLS-1$ video.addPropertyChangeListener("image", this); //$NON-NLS-1$ video.addPropertyChangeListener("filterChanged", this); //$NON-NLS-1$ video.addPropertyChangeListener("videoVisible", this); //$NON-NLS-1$ video.addPropertyChangeListener("size", this); //$NON-NLS-1$ // synchronize coords if(video.isMeasured()) { coords = video.getCoords(); } else { video.setCoords(coords); } synchronized(drawableList) { drawableList.add(0, video); // put video at back } BufferedImage vidImage = video.getImage(); if(vidImage!=null) { setImageWidth(vidImage.getWidth()); setImageHeight(vidImage.getHeight()); } } repaint(); } } /** * Imports Data from a source into a DataTrack. * This method returns null, but superclass TrackerPanel overrides it * and returns a DataTrack track. * Source object may be String path, JPanel controlPanel, Tool tool, etc * * @param data the Data to import * @param source the data source (may be null) * @return the DataTrack with the Data (may return null) */ public DataTrack importData(Data data, Object source) { return null; } /** * Overrides DrawingPanel paintEverything method. * * @param g the graphics context to draw on */ protected void paintEverything(Graphics g) { // increase bottom gutter to make room for the player if(playerVisible) { bottomGutter += player.height; } super.paintEverything(g); // restore bottom gutter if(playerVisible) { bottomGutter -= player.height; } } /** * Overrides DrawingPanel scale method to handle drawing in imagespace * * @param drawables the list of drawable objects */ protected void scale(ArrayList<Drawable> drawables) { if(drawingInImageSpace) { // scale to image units xminPreferred = -imageBorder*imageWidth+xOffset; xmaxPreferred = imageWidth+imageBorder*imageWidth+xOffset; yminPreferred = imageHeight+imageBorder*imageHeight+yOffset; ymaxPreferred = -imageBorder*imageHeight+yOffset; } super.scale(drawables); } /** * Overrides DrawingPanel checkImage method so offscreenImage will not include * the videoPlayer. * * @return <code>true</code> if the image is correctly sized */ protected boolean checkImage() { Dimension d = getSize(); if(playerVisible) { d.height -= player.height; // don't include player area } if((d.width<=2)||(d.height<=2)) { return false; // image is too small } if((offscreenImage==null)||(d.width!=offscreenImage.getWidth())||(d.height!=offscreenImage.getHeight())) { offscreenImage = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_RGB); } if(offscreenImage==null) { return false; } return true; // the buffered image exists and is the correct size } /** * Gets the world coordinates of the last mouse event * * @return world coordinates of last mouse event */ public Point2D getWorldMousePoint() { // get coordinates of mouse pt.setLocation(getMouseX(), getMouseY()); // transform if nec if(isDrawingInImageSpace()) { int n = getFrameNumber(); AffineTransform toWorld = getCoords().getToWorldTransform(n); toWorld.transform(pt, pt); } return pt; } //______________________________ object loader ________________________________ /** * Returns an XML.ObjectLoader to save and load data for this object. * * @return the object loader */ public static XML.ObjectLoader getLoader() { return new Loader(); } /** * A class to save and load data for this object. */ static class Loader implements XML.ObjectLoader { /** * Saves object data to an XMLControl. * * @param control the control to save to * @param obj the object to save */ public void saveObject(XMLControl control, Object obj) { VideoPanel vidPanel = (VideoPanel) obj; // save the video clip and coords control.setValue("videoclip", vidPanel.getPlayer().getVideoClip()); //$NON-NLS-1$ control.setValue("coords", vidPanel.getCoords()); //$NON-NLS-1$ // save the drawables ArrayList<Drawable> list = vidPanel.getDrawables(); list.remove(vidPanel.getVideo()); // the video is saved by videoclip if(!list.isEmpty()) { control.setValue("drawables", list); //$NON-NLS-1$ } } /** * Creates an object using data from an XMLControl. * * @param control the control * @return the newly created object */ public Object createObject(XMLControl control) { return new VideoPanel(); } /** * Loads an object with data from an XMLControl. * * @param control the control * @param obj the object * @return the loaded object */ public Object loadObject(XMLControl control, Object obj) { VideoPanel vidPanel = (VideoPanel) obj; // load the video clip VideoClip clip = (VideoClip) control.getObject("videoclip"); //$NON-NLS-1$ if(clip!=null) { vidPanel.getPlayer().setVideoClip(clip); } // load the coords vidPanel.setCoords((ImageCoordSystem) control.getObject("coords")); //$NON-NLS-1$ // load the drawables Collection<?> drawables = (Collection<?>) control.getObject("drawables"); //$NON-NLS-1$ if(drawables!=null) { Iterator<?> it = drawables.iterator(); while(it.hasNext()) { vidPanel.addDrawable((Drawable) it.next()); } } return obj; } } } /* * Open Source Physics software is free software; you can redistribute * it and/or modify it under the terms of the GNU General Public License (GPL) as * published by the Free Software Foundation; either version 2 of the License, * or(at your option) any later version. * Code that uses any portion of the code in the org.opensourcephysics package * or any subpackage (subdirectory) of this package must must also be be released * under the GNU GPL license. * * This software 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; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA * or view the license online at http://www.gnu.org/copyleft/gpl.html * * Copyright (c) 2007 The Open Source Physics project * http://www.opensourcephysics.org */