/*
 * 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) 2004  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.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Collection;
import java.util.HashMap;
import javax.swing.event.SwingPropertyChangeSupport;
import org.opensourcephysics.display.DrawingPanel;
import org.opensourcephysics.display.Interactive;

/**
 * This provides basic implementations of all Video methods. Subclasses should
 * provide a raw image for display--see ImageVideo or GifVideo for an example.
 *
 * @author Douglas Brown
 * @version 1.0
 */
public class VideoAdapter implements Video, PropertyChangeListener {
  // instance fields
  protected Image rawImage;              // raw image from video source
  protected Dimension size;              // image pixel dimensions
  protected BufferedImage bufferedImage; // offscreen buffered image copy
  protected BufferedImage filteredImage; // filtered image
  protected int frameCount;
  protected int frameNumber;
  protected int startFrameNumber;
  protected int endFrameNumber;
  protected double rate = 1;
  protected boolean playing = false;
  protected boolean looping = false;
  protected double minX, maxX, minY, maxY;
  protected boolean mouseEnabled = false;
  protected boolean visible = true;
  protected boolean isMeasured = false;
  protected boolean isValidMeasure = false;
  protected boolean widthDominates = true;
  protected boolean isValidImage = false;
  protected boolean isValidFilteredImage = false;
  protected ImageCoordSystem coords;
  protected DoubleArray aspects;
  protected PropertyChangeSupport support;
  protected HashMap<String, Object> properties = new HashMap<String, Object>();
  protected FilterStack filterStack = new FilterStack();
  protected Raster clearRaster;

  /**
   * Protected constructor creates an empty VideoAdapter
   */
  protected VideoAdapter() {
    initialize();
  }

  /**
   * Draws the video image on the panel.
   *
   * @param panel the drawing panel requesting the drawing
   * @param g the graphics context on which to draw
   */
  public void draw(DrawingPanel panel, Graphics g) {
    if(!visible) {
      return;
    }
    Graphics2D g2 = (Graphics2D) g;
    if(((panel instanceof VideoPanel)&&((VideoPanel) panel).isDrawingInImageSpace())||isMeasured) {
      AffineTransform gat = g2.getTransform(); // save graphics transform
      g2.transform(panel.getPixelTransform()); // world to screen
      if(panel instanceof VideoPanel) {
        VideoPanel vidPanel = (VideoPanel) panel;
        if(!vidPanel.isDrawingInImageSpace()) {
          // use video panel's coords for vid to world transform
          ImageCoordSystem coords = vidPanel.getCoords();
          g2.transform(coords.getToWorldTransform(frameNumber));
        }
      } else {
        // use this video's coords for vid to world transform
        g2.transform(coords.getToWorldTransform(frameNumber));
      }
      // draw the video or filtered image
      if(filterStack.isEmpty()||!filterStack.isEnabled()) {
        g2.drawImage(rawImage, 0, 0, panel);
      } else {
        g2.drawImage(getImage(), 0, 0, panel);
      }
      g2.setTransform(gat);                    // restore transform
    } else {                                   // center image in panel if not measured
      double centerX = (panel.getXMax()+panel.getXMin())/2;
      double centerY = (panel.getYMax()+panel.getYMin())/2;
      int xoffset = panel.xToPix(centerX)-size.width/2;
      int yoffset = panel.yToPix(centerY)-size.height/2;
      // draw the video or filtered image
      if(filterStack.isEmpty()||!filterStack.isEnabled()) {
        g2.drawImage(rawImage, xoffset, yoffset, panel);
      } else {
        g2.drawImage(getImage(), xoffset, yoffset, panel);
      }
    }
  }

  /**
   * Shows or hides the video.
   *
   * @param visible <code>true</code> to show the video
   */
  public void setVisible(boolean visible) {
    this.visible = visible;
    firePropertyChange("videoVisible", null, new Boolean(visible)); //$NON-NLS-1$
  }

  /**
   * Gets the visibility of the video.
   *
   * @return <code>true</code> if the video is visible
   */
  public boolean isVisible() {
    return visible;
  }

  /**
   * Gets the minimum x needed to draw this object.
   *
   * @return minimum x
   */
  public double getXMin() {
    if(!isValidMeasure) {
      findMinMaxValues();
    }
    return minX;
  }

  /**
   * Gets the maximum x needed to draw this object.
   *
   * @return maximum x
   */
  public double getXMax() {
    if(!isValidMeasure) {
      findMinMaxValues();
    }
    return maxX;
  }

  /**
   * Gets the minimum y needed to draw this object.
   *
   * @return minimum y
   */
  public double getYMin() {
    if(!isValidMeasure) {
      findMinMaxValues();
    }
    return minY;
  }

  /**
   * Gets the maximum y needed to draw this object.
   *
   * @return maximum y
   */
  public double getYMax() {
    if(!isValidMeasure) {
      findMinMaxValues();
    }
    return maxY;
  }

  /**
   * Reports whether information is available to set min/max values.
   *
   * @return <code>true</code> if min/max values are valid
   */
  public boolean isMeasured() {
    return isMeasured;
  }

  /**
   * Gets the current video image after applying enabled filters.
   *
   * @return the current video image with filters applied
   */
  public BufferedImage getImage() {
    if(!isValidImage) { // bufferedImage needs refreshing
      isValidImage = true;
      Graphics g = bufferedImage.createGraphics();
      bufferedImage.setData(clearRaster);
      g.drawImage(rawImage, 0, 0, null);
    }
    if(filterStack.isEmpty()||!filterStack.isEnabled()) {
      return bufferedImage;
    } else if(!isValidFilteredImage) { // filteredImage needs refreshing
      isValidFilteredImage = true;
      filteredImage = filterStack.getFilteredImage(bufferedImage);
    }
    return filteredImage;
  }

  /**
   * Returns this video if enabled.
   *
   * @param panel the drawing panel
   * @param xpix the x coordinate in pixels
   * @param ypix the y coordinate in pixels
   * @return this if enabled, otherwise null
   */
  public Interactive findInteractive(DrawingPanel panel, int xpix, int ypix) {
    if(!mouseEnabled) {
      return null;
    }
    return this;
  }

  /**
   * Sets whether this responds to mouse hits.
   *
   * @param enabled <code>true</code> if this responds to mouse hits.
   */
  public void setEnabled(boolean enabled) {
    mouseEnabled = enabled;
  }

  /**
   * Gets whether this responds to mouse hits.
   *
   * @return <code>true</code> if this responds to mouse hits.
   */
  public boolean isEnabled() {
    return mouseEnabled;
  }

  /**
   * Sets x position of upper left corner of the specified video frame
   * in world units.
   *
   * @param n the video frame number
   * @param x the world x position
   */
  public void setFrameX(int n, double x) {
    setFrameXY(n, x, coords.imageToWorldY(n, 0, 0));
  }

  /**
   * Sets x position of upper left corner of all video frames
   * in world units.
   *
   * @param x the world x position
   */
  public void setX(double x) {
    for(int n = 0; n<frameCount; n++) {
      setFrameX(n, x);
    }
  }

  /**
   * Sets y position of upper left corner of the specified video frame
   * in world units.
   *
   * @param n the video frame number
   * @param y the world y position
   */
  public void setFrameY(int n, double y) {
    setFrameXY(n, coords.imageToWorldX(n, 0, 0), y);
  }

  /**
   * Sets y position of upper left corner of all video frames
   * in world units.
   *
   * @param y the world y position
   */
  public void setY(double y) {
    for(int n = 0; n<frameCount; n++) {
      setFrameY(n, y);
    }
  }

  /**
   * Gets x position of upper left corner of the current video frame
   * in world units.
   *
   * @return the world x position
   */
  public double getX() {
    return coords.imageToWorldX(frameNumber, 0, 0);
  }

  /**
   * Gets y position of upper left corner of the current video frame
   * in world units.
   *
   * @return the world y position
   */
  public double getY() {
    return coords.imageToWorldY(frameNumber, 0, 0);
  }

  /**
   * Sets the x and y position of the UL corner of the specified video
   * frame in world units.
   *
   * @param n the video frame number
   * @param x the world x position
   * @param y the world y position
   */
  public void setFrameXY(int n, double x, double y) {
    double sin = coords.getSine(n);
    double cos = coords.getCosine(n);
    double tx = coords.getScaleX(n)*(y*sin-x*cos);
    double ty = coords.getScaleY(n)*(y*cos+x*sin);
    coords.setOriginXY(n, tx, ty);
  }

  /**
   * Sets the x and y position of the UL corner of all video frames
   * in world units.
   *
   * @param x the world x position
   * @param y the world y position
   */
  public void setXY(double x, double y) {
    for(int n = 0; n<frameCount; n++) {
      setFrameXY(n, x, y);
    }
  }

  /**
   * Sets the relative aspect of the specified video frame. Relative
   * aspect is the ratio of the world aspect to the pixel aspect of
   * the image. The pixel aspect is the ratio of image width to height
   * in pixels, and world aspect is the ratio of world width to height
   * in world units. For example, a 320 x 240 pixel movie has a pixel
   * aspect of 1.33. If relative aspect is 2, then the world aspect
   * will be 2.67. So if the video's width is 16 wu, its height will
   * be 6 wu. Or if its height is 10 wu, its width will be 26.67 wu.
   *
   * @param n the video frame number
   * @param relativeAspect the desired relative aspect
   */
  public void setFrameRelativeAspect(int n, double relativeAspect) {
    if((relativeAspect<0.001)||(relativeAspect>1000)) {
      return;
    }
    aspects.set(n, Math.abs(relativeAspect));
    if(isMeasured) {
      if(widthDominates) {
        setFrameWidth(n, size.width/coords.getScaleX(n));
      } else {
        setFrameHeight(n, size.height/coords.getScaleY(n));
      }
    }
  }

  /**
   * Sets the relative aspect of all video frames. Relative
   * aspect is the ratio of the world aspect to the pixel aspect of
   * the image. The pixel aspect is the ratio of image width to height
   * in pixels, and world aspect is the ratio of world width to height
   * in world units. For example, a 320 x 240 pixel movie has a pixel
   * aspect of 1.33. If relative aspect is 2, then the world aspect
   * will be 2.67. So if the video's width is 16 wu, its height will
   * be 6 wu. Or if its height is 10 wu, its width will be 26.67 wu.
   *
   * @param relativeAspect the desired relative aspect
   */
  public void setRelativeAspect(double relativeAspect) {
    for(int n = 0; n<frameCount; n++) {
      setFrameRelativeAspect(n, relativeAspect);
    }
  }

  /**
   * Gets the relative aspect of the current video frame.
   *
   * @return the relative aspect of the current image.
   */
  public double getRelativeAspect() {
    return aspects.get(frameNumber);
  }

  /**
   * Sets the width of the specified video frame in world units. Also sets
   * the height using the relative aspect.
   *
   * @param n the video frame number
   * @param width the width in world units
   * @see #setRelativeAspect
   */
  public void setFrameWidth(int n, double width) {
    if(width==0) {
      return;
    }
    width = Math.abs(width);
    // save x and y since setting width invalidates them
    double x = coords.imageToWorldX(n, 0, 0);
    double y = coords.imageToWorldY(n, 0, 0);
    double scaleX = size.width/width;
    coords.setScaleX(n, scaleX);
    coords.setScaleY(n, scaleX*aspects.get(n));
    widthDominates = true;
    // restore x and y to their correct values
    setFrameXY(n, x, y);
  }

  /**
   * Sets the width of all video frames in world units. Also sets
   * the heights using the relative aspect.
   *
   * @param width the width in world units
   * @see #setRelativeAspect
   */
  public void setWidth(double width) {
    for(int n = 0; n<frameCount; n++) {
      setFrameWidth(n, width);
    }
  }

  /**
   * Gets the current width of the video frame.
   *
   * @return the width of the video image
   */
  public double getWidth() {
    return size.width/coords.getScaleX(frameNumber);
  }

  /**
   * Sets the height of the specified video frame in world units. Also sets
   * the width using the relative aspect.
   *
   * @param n the video frame number
   * @param height the height in world units
   * @see #setRelativeAspect
   */
  public void setFrameHeight(int n, double height) {
    if(height==0) {
      return;
    }
    height = Math.abs(height);
    // save x and y since setting width invalidates them
    double x = coords.imageToWorldX(n, 0, 0);
    double y = coords.imageToWorldY(n, 0, 0);
    double scaleY = size.height/height;
    coords.setScaleY(n, scaleY);
    coords.setScaleX(n, scaleY/aspects.get(n));
    widthDominates = false;
    // restore x and y to their correct values
    setFrameXY(n, x, y);
  }

  /**
   * Sets the height of all video frames in world units. Also sets
   * the widths using the relative aspect.
   *
   * @param height the height in world units
   * @see #setRelativeAspect
   */
  public void setHeight(double height) {
    for(int n = 0; n<frameCount; n++) {
      setFrameHeight(n, height);
    }
  }

  /**
   * Gets the current height of the video frame.
   *
   * @return the height of the video image
   */
  public double getHeight() {
    return size.height/coords.getScaleY(frameNumber);
  }

  /**
   * Sets the angle in radians of the specified video frame measured ccw
   * from the world x-axis. This results in a rotation only.
   *
   * @param n the video frame number
   * @param theta the angle in radians
   */
  public void setFrameAngle(int n, double theta) {
    // save x and y since setting angle invalidates them
    double x = coords.imageToWorldX(n, 0, 0);
    double y = coords.imageToWorldY(n, 0, 0);
    double cos = Math.cos(theta);
    double sin = Math.sin(theta);
    coords.setCosineSine(n, cos, -sin);
    setFrameXY(n, x, y); // restore x and y to their correct values
  }

  /**
   * Sets the angle in radians of all video frames measured ccw
   * from the world x-axis. This results in a rotation only.
   *
   * @param theta the angle in radians
   */
  public void setAngle(double theta) {
    for(int n = 0; n<frameCount; n++) {
      setFrameAngle(n, theta);
    }
  }

  /**
   * Gets the angle in radians of the curent video frame measured ccw
   * from the world x-axis.
   *
   * @return the angle in radians
   */
  public double getAngle() {
    return -coords.getAngle(frameNumber);
  }

  /**
   * Steps the video forward one frame.
   */
  public void step() {
    stop();
    setFrameNumber(frameNumber+1);
  }

  /**
   * Steps the video back one frame.
   */
  public void back() {
    stop();
    setFrameNumber(frameNumber-1);
  }

  /**
   * Gets the total number of video frames.
   *
   * @return the number of video frames
   */
  public int getFrameCount() {
    return frameCount;
  }

  /**
   * Gets the current video frame number.
   *
   * @return the current frame number
   */
  public int getFrameNumber() {
    return frameNumber;
  }

  /**
   * Sets the video frame number.
   *
   * @param n the desired frame number
   */
  public void setFrameNumber(int n) {
    if(n==frameNumber) {
      return;
    }
    n = Math.min(n, endFrameNumber);
    n = Math.max(n, startFrameNumber);
    frameNumber = n;
  }

  /**
   * Gets the start frame number.
   *
   * @return the start frame number
   * @see #getEndFrameNumber
   */
  public int getStartFrameNumber() {
    return startFrameNumber;
  }

  /**
   * Sets the start frame number.
   *
   * @param n the desired start frame number
   * @see #setEndFrameNumber
   */
  public void setStartFrameNumber(int n) {
    if(n==startFrameNumber) {
      return;
    }
    n = Math.max(0, n);
    startFrameNumber = Math.min(endFrameNumber, n);
    firePropertyChange("startframe", null, new Integer(startFrameNumber)); //$NON-NLS-1$
  }

  /**
   * Gets the end frame number.
   *
   * @return the end frame number
   * @see #getStartFrameNumber
   */
  public int getEndFrameNumber() {
    return endFrameNumber;
  }

  /**
   * Sets the end frame number.
   *
   * @param n the desired end frame number,
   * @see #setStartFrameNumber
   */
  public void setEndFrameNumber(int n) {
    if(n==endFrameNumber) {
      return;
    }
    if(frameCount>1) {
      n = Math.min(frameCount-1, n);
    }
    endFrameNumber = Math.max(startFrameNumber, n);
    firePropertyChange("endframe", null, new Integer(endFrameNumber)); //$NON-NLS-1$
  }

  /**
   * Gets the start time of the specified frame in milliseconds.
   *
   * @param n the frame number
   * @return the start time of the frame in milliseconds, or -1 if not known
   */
  public double getFrameTime(int n) {
    return -1;
  }

  /**
   * Gets the duration of the specified frame in milliseconds.
   *
   * @param n the frame number
   * @return the duration of the frame in milliseconds
   */
  public double getFrameDuration(int n) {
    if(frameCount==1) {
      return getDuration();
    }
    if(n==frameCount-1) {
      return getDuration()-getFrameTime(n);
    }
    return getFrameTime(n+1)-getFrameTime(n);
  }

  /**
   * Plays the video at the current rate.
   */
  public void play() {
    playing = true;
  }

  /**
   * Stops the video.
   */
  public void stop() {
    playing = false;
  }

  /**
   * Stops the video and resets it to the start time.
   */
  public void reset() {
    stop();
    setFrameNumber(startFrameNumber);
  }

  /**
   * Gets the current video time in milliseconds.
   *
   * @return the current time in milliseconds, or -1 if not known
   */
  public double getTime() {
    return -1;
  }

  /**
   * Sets the video time in milliseconds.
   *
   * @param millis the desired time in milliseconds
   */
  public void setTime(double millis) {

  /** implemented by subclasses */
  }

  /**
   * Gets the start time in milliseconds.
   *
   * @return the start time in milliseconds, or -1 if not known
   */
  public double getStartTime() {
    return -1;
  }

  /**
   * Sets the start time in milliseconds. NOTE: the actual start time
   * is normally set to the beginning of a frame.
   *
   * @param millis the desired start time in milliseconds
   */
  public void setStartTime(double millis) {

  /** implemented by subclasses */
  }

  /**
   * Gets the end time in milliseconds.
   *
   * @return the end time in milliseconds, or -1 if not known
   */
  public double getEndTime() {
    return -1;
  }

  /**
   * Sets the end time in milliseconds. NOTE: the actual end time
   * is set to the end of a frame.
   *
   * @param millis the desired end time in milliseconds
   */
  public void setEndTime(double millis) {

  /** implemented by subclasses */
  }

  /**
   * Gets the duration of the video.
   *
   * @return the duration of the video in milliseconds, or -1 if not known
   */
  public double getDuration() {
    return -1;
  }

  /**
   * Sets the frame number to the start frame.
   */
  public void goToStart() {
    setFrameNumber(startFrameNumber);
  }

  /**
   * Sets the frame number to the end frame.
   */
  public void goToEnd() {
    setFrameNumber(endFrameNumber);
  }

  /**
   * Starts and stops the video.
   *
   * @param playing <code>true</code> starts the video, and
   * <code>false</code> stops it
   */
  public void setPlaying(boolean playing) {
    if(playing) {
      play();
    } else {
      stop();
    }
  }

  /**
   * Gets the playing state of this video.
   *
   * @return <code>true</code> if the video is playing
   */
  public boolean isPlaying() {
    return playing;
  }

  /**
   * Sets the looping state of this video.
   * If true, the video restarts when reaching the end.
   *
   * @param loops <code>true</code> if the video loops
   */
  public void setLooping(boolean loops) {
    if(looping==loops) {
      return;
    }
    looping = loops;
    firePropertyChange("looping", null, new Boolean(looping)); //$NON-NLS-1$
  }

  /**
   * Gets the looping state of the video.
   * If true, the video restarts when reaching the end.
   *
   * @return <code>true</code> if the video loops
   */
  public boolean isLooping() {
    return looping;
  }

  /**
   * Sets the relative play rate. Relative play rate is the ratio
   * of a video's play rate to its preferred ("normal") play rate.
   *
   * @param rate the relative play rate.
   */
  public void setRate(double rate) {
    rate = Math.abs(rate);
    if((rate==this.rate)||(rate==0)) {
      return;
    }
    this.rate = rate;
  }

  /**
   * Gets the relative play rate. Relative play rate is the ratio
   * of a video's play rate to its preferred ("normal") play rate.
   *
   * @return the relative play rate.
   */
  public double getRate() {
    return rate;
  }

  /**
   * Sets the image coordinate system used to convert from
   * imagespace to worldspace.
   *
   * @param coords the image coordinate system
   */
  public void setCoords(ImageCoordSystem coords) {
    if(coords==this.coords) {
      return;
    }
    this.coords.removePropertyChangeListener(this);
    coords.addPropertyChangeListener(this);
    this.coords = coords;
    isMeasured = true;
    isValidMeasure = false;
    firePropertyChange("coords", null, coords); //$NON-NLS-1$
  }

  /**
   * Gets the image coordinate system.
   *
   * @return the image coordinate system
   */
  public ImageCoordSystem getCoords() {
    return coords;
  }

  /**
   * Sets the filter stack.
   *
   * @param stack the new filter stack
   */
  public void setFilterStack(FilterStack stack) {
    filterStack.removePropertyChangeListener(this);
    filterStack = stack;
    filterStack.addPropertyChangeListener(this);
  }

  /**
   * Gets the filter stack.
   *
   * @return the filter stack
   */
  public FilterStack getFilterStack() {
    return filterStack;
  }

  /**
   * Sets a user property of the video.
   *
   * @param name the name of the property
   * @param value the value of the property
   */
  public void setProperty(String name, Object value) {
    if(name.equals("measure")) { //$NON-NLS-1$
      isValidMeasure = false;
    } else {
      properties.put(name, value);
    }
  }

  /**
   * Gets a user property of the video. May return null.
   *
   * @param name the name of the property
   * @return the value of the property
   */
  public Object getProperty(String name) {
    return properties.get(name);
  }

  /**
   * Gets a collection of user property names for the video.
   *
   * @return a collection of property names
   */
  public Collection<String> getPropertyNames() {
    return properties.keySet();
  }

  /**
   * Adds a PropertyChangeListener to this video.
   *
   * @param listener the object requesting property change notification
   */
  public void addPropertyChangeListener(PropertyChangeListener listener) {
    support.addPropertyChangeListener(listener);
  }

  /**
   * Adds a PropertyChangeListener to this video.
   *
   * @param property the name of the property of interest to the listener
   * @param listener the object requesting property change notification
   */
  public void addPropertyChangeListener(String property, PropertyChangeListener listener) {
    support.addPropertyChangeListener(property, listener);
  }

  /**
   * Removes a PropertyChangeListener from this video.
   *
   * @param listener the listener requesting removal
   */
  public void removePropertyChangeListener(PropertyChangeListener listener) {
    support.removePropertyChangeListener(listener);
  }

  /**
   * Removes a PropertyChangeListener for a specified property.
   *
   * @param property the name of the property
   * @param listener the listener to remove
   */
  public void removePropertyChangeListener(String property, PropertyChangeListener listener) {
    support.removePropertyChangeListener(property, listener);
  }

  /**
   * Disposes of this video.
   */
  public void dispose() {
  	if (coords!=null)
  		coords.removePropertyChangeListener(this);
    getFilterStack().setInspectorsVisible(false);
  }

  /**
   * Responds to property change events. VideoAdapter listens for the following
   * events: "transform" from ImageCoordSystem and "image" from
   * FilterStack.
   *
   * @param e the property change event
   */
  public void propertyChange(PropertyChangeEvent e) {
    if(e.getSource()==coords) {             // "transform"
      isMeasured = true;
      isValidMeasure = false;
    } else if(e.getSource()==filterStack) { // "image"
      isValidFilteredImage = false;
      support.firePropertyChange(e);
    }
  }

  //____________________________ protected methods ____________________________

  /**
   * Sends a PropertyChangeEvent to registered listeners. No event is sent
   * if oldVal and newVal are equal, unless they are both null.
   *
   * @param property the name of the property that has changed
   * @param oldVal the value of the property before the change (may be null)
   * @param newVal the value of the property after the change (may be null)
   */
  protected void firePropertyChange(String property, Object oldVal, Object newVal) {
    support.firePropertyChange(property, oldVal, newVal);
  }

  /**
   * Called by the garbage collector when this video is no longer in use.
   */
  protected void finalize() {
    dispose();
  }

  //_______________________________ protected methods _________________________

  /**
   * Initialize this video.
   */
  protected void initialize() {
    support = new SwingPropertyChangeSupport(this);
    filterStack.addPropertyChangeListener(this);
  }

  /**
   * Refreshes the BufferedImage based on current size.
   * Creates a new image if needed.
   */
  protected void refreshBufferedImage() {
    if((bufferedImage==null)||(bufferedImage.getWidth()!=size.width)||(bufferedImage.getHeight()!=size.height)) {
      bufferedImage = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_RGB);
      int clear = new Color(0, 0, 0, 0).getRGB();
      int[] rgb = new int[size.width*size.height];
      for(int i = 0; i<rgb.length; i++) {
        rgb[i] = clear;
      }
      bufferedImage.setRGB(0, 0, size.width, size.height, rgb, 0, size.width);
      clearRaster = bufferedImage.getData();
      isValidImage = false;
    }
  }

  /**
   * Finds the min and max values of x and y.
   */
  protected void findMinMaxValues() {
    VideoClip clip = (VideoClip) getProperty("videoclip"); //$NON-NLS-1$
    // check all four corner positions of every frame in the current clip
    Point2D corner = new Point2D.Double(0, 0);             // top left
    int start = 0;
    if(clip!=null) {
      start = clip.getStartFrameNumber();
    }
    AffineTransform at = coords.getToWorldTransform(start);
    at.transform(corner, corner);
    maxX = minX = corner.getX();
    maxY = minY = corner.getY();
    int stepCount = frameCount;
    if(clip!=null) {
      stepCount = clip.getStepCount();
    }
    for(int n = 0; n<stepCount; n++) {
      if(clip==null) {
        at = coords.getToWorldTransform(n);
      } else {
        at = coords.getToWorldTransform(clip.stepToFrame(n));
      }
      for(int i = 0; i<4; i++) {
        switch(i) {
           case 0 :
             corner.setLocation(0, 0);
             break;
           case 1 :
             corner.setLocation(size.width, 0);
             break;
           case 2 :
             corner.setLocation(0, size.height);
             break;
           case 3 :
             corner.setLocation(size.width, size.height);
        }
        at.transform(corner, corner);
        minX = Math.min(corner.getX(), minX);
        maxX = Math.max(corner.getX(), maxX);
        minY = Math.min(corner.getY(), minY);
        maxY = Math.max(corner.getY(), maxY);
      }
    }
    isValidMeasure = true;
  }

}

/*
 * 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
 */
