/* This file is part of the source code for 3D-XplorMath-J, Version 1.0 (January 2008).
* Copyright (c) 2008 The 3D-XplorMath Consortium (http://3d-xplormath.org).
* This source code is released under a BSD License, which allows redistribution
* in source and binary form, with or without modification, provided copyright
* and license information are included, and with no warranty or guarantee of
* any kind. For details, see http://3d-xplormath.org/j/source/BSDLicense.txt
*/
package vmm.core;
import java.util.ArrayList;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* An animation that runs in its own thread, separate from the Swing user interface
* thread. Because Swing is not thread-safe and the animation runs in its own thread,
* it is not safe to call arbitrary Swing and Graphics2D methods. (If it is necessary
* to call them, the invokeAndWait or invokeLater methods
* from class javax.swing.SwingUtilities can be used.) However, calling
* repaint is OK. Since setting parameter values and adding or removing
* decorations generate calls to repaint, they are also safe.
* Note that an animation does not start running automatically, but only when its
* {@link #start()} method is called.
*
To create a ThreadedAnimation, it is usually only necessary to create a subclass
* and implement {@link #runAnimation()}, the single abstract method defined in this
* class. This method is a script for the animation, which is run from beginning to end.
* When this method returns, the animation ends; if the method never returns, then the
* animation must be canceled by some external agent that calls its cancel
* method.
*
The runAnimation method can call the {@link #pause(int)} method to insert
* a pause into the animation. Behind the scenes, this method also checks to see whether
* the animation has been canceled. If so, it throws an exception that aborts the animation.
* To avoid delays between the time when the animation's cancel method is called and
* tha time when the animation actually stops, it is important that pause be called
* regularly. If no delay is desired, pause can be called with a parameter of zero.
*
Some animations will have some "clean-up" to do when the animation ends, whether it
* ends because the runAnimation method returns or because it has been canceled.
* to make sure that the clean-up is done in all cases, it is advisable to do the clean-up in
* a finally clause in the runAnimation method. For a simple
* example of this, see {@link vmm.planecurve.parametric.OsculatingCircleAnimation#runAnimation()}.
*
A ThreadedAnimation emits ChangeEvents when it is started and when it ends. A ChangeListener
* can tell which event generated the ChangeEvent by calling the (Note that if some error other than an In addition to checking for cancellation, this method also checks to see whether the
* animation has been paused by calling its isRunning method of
* the animation. Note that in some circumstances, an alternative method for doing set-up and
* clean-up for the animation is to install a ChangeListener that does the set-up/clean-up
* in response to ChangeEvents.
*/
abstract public class ThreadedAnimation implements Animation {
private Thread runner;
volatile private boolean running, canceled, paused;
volatile double timeDilation;
private ChangeEvent changeEvent;
private ArrayListpause
* regularly. The animation ends when this method returns or the first time pause is
* called after the cancel method has been called; the pause method
* generates an {@link AnimationCanceledException} in this case. If there is clean-up that
* must be done when the animation ends, it is advisable to do it in a finally
* clause in this method.
* AnimationCanceledException occurs
* during the animation, it will also abort the animation. Since this is presumably a programming
* error, a stack trace for the exception is printed to standard output.)
* @see #pause(int)
* @see #cancel()
*/
abstract protected void runAnimation();
/**
* A trivial exception class that exists only to make it possible to cancel ThreadAnimations.
* If an animation's cancel method is called, then an AnimationCanceledException is
* thrown the next time pause is called. (Programming note: The cancel
* method can't throw the exception itself because it is presumably being called in another
* thread -- the exception would abort that thread instead of the animation thread.)
*/
protected class AnimationCanceledException extends RuntimeException {
}
/**
* Can be called in runAnimation to insert a delay into an animation. This method
* also checks whether the cancel method has been called. If so, it will throw
* an excpetion of type {@link AnimationCanceledException}. This method can be called with
* a parameter of zero to check for cancelation without inserting a delay (in this case, it
* calls Thread.yield to give other threads that are waiting for CPU time
* an opportunity to run. It is important that this method be called regularly during the course
* of an animation so that cancelations will take effect in a timely manner.
* setPaused method. If so, this
* method will wait for the animtion to be unpaused before returning. Again, it is important
* for pause to be called regularly for setPaused to take effect
* in a timely way.
* @param milliseconds a delay equal to this many milliseconds, if the animation is running at normal speed,
* is inserted into the animation. If a time dilation factor other than 1 has been set, however, then
* this parameter is multiplied by the time dilation factor to give the actual number of milliseconds
* used in the delay. A delay of zero will cause Thread.yield to be called, which gives
* other threads a chance to run.
* @see #setTimeDilation(double)
* @see #runAnimation()
* @see #cancel()
* @see #setPaused(boolean)
*/
protected void pause(int milliseconds) throws AnimationCanceledException {
if (canceled)
throw new AnimationCanceledException();
int delay = (int)(milliseconds*timeDilation + 0.49);
if (milliseconds > 0 && delay == 0)
delay = 1;
if (delay <= 0)
Thread.yield();
else {
synchronized(this) {
try {
wait(delay);
}
catch (InterruptedException e) {
}
}
}
if (canceled)
throw new AnimationCanceledException();
if (paused) {
synchronized(this) {
while (paused && !canceled) {
try {
wait();
}
catch (InterruptedException e) {
}
}
}
if (canceled)
throw new AnimationCanceledException();
}
}
/**
* This method must be called to start the animation running. An animation does not start
* automatically, but only when this method is called. If the animation is already running,
* this has no effect. If the animation has already run and ended, this will restart thea
* animation from the beginning. The animation will run until the runAnimation
* ends or until the animation is canceled.
*/
synchronized public void start() {
if (running)
return;
runner = new Thread() {
public void run() {
doRun();
}
};
runner.start();
running = true;
canceled = false;
paused = false;
fireAnimationChangeEvent();
}
/**
* Pauses or unpauses a running animation. If the animation is not running, this has no effect.
* @param paused if true, then the animation is paused; if false, then the animation is unpaused
*/
synchronized public void setPaused(boolean paused) {
if (running) {
this.paused = paused;
notify();
}
}
/**
* Tests whether the animation is paused. Only a running animation can be paused.
*/
public boolean isPaused() {
return paused;
}
/**
* Cancels the animation. The animation will be stopped as soon as possible (that is, the next
* time the {@link #pause(int)} method is called by {@link #runAnimation()}). If the animation is
* not running, this has no effect.
*/
synchronized public void cancel() {
if (!running)
return;
canceled = true;
notify();
}
/**
* Tests whether the animation is running.
*/
public boolean isRunning() {
return running;
}
/**
* Tests whether the animation has been canceled. This can be called after the animation ends to determine
* whether the animation ended on its own or because it was canceled.
*/
public boolean wasCanceled() {
return canceled;
}
/**
* Slows down or speeds up the animation by multiplying all delay times (as specified in the parameter
* the pause method) by a time dilation factor. Note that the dilation applies only to
* delay times, not to processing times, so the animation speed is only approximately multiplied by
* the dilation factor. In particular, only a limited amout of speed-up can be obtained, no matter
* how close to zero you make the dilation factor. The default value of the time dilation factor
* is 1, which corresponds to normal run speed.
* @param dilationFactor delay times for the pause method are multiplied by this factor to
* give the actual time delay. A dilationFactor less than zero is treated as zero.
* @see #pause(int)
*/
public void setTimeDilation(double dilationFactor) {
if (dilationFactor < 0)
dilationFactor = 0;
timeDilation = dilationFactor;
}
/**
* Returns the time dilation factor that is currently set for this animation.
* @see #setTimeDilation(double)
*/
public double getTimeDilation() {
return timeDilation;
}
private void doRun() {
try {
runAnimation();
}
catch (AnimationCanceledException e) {
}
catch (Exception e) {
System.out.println("Animation aborted by unexpected exception: " + e);
e.printStackTrace();
}
finally {
doneRunning();
}
}
synchronized private void doneRunning() {
running = false;
paused = false;
runner = null;
fireAnimationChangeEvent();
}
//----------------- support for ChangeEvents --------------------------------------
/**
* Add a ChangeListener to this animation. Change events are sent when the animation
* stops and when it stops for any reason. The ChangeListener can distinguish the two events
* by calling the isRunning method, which will retrun true if the
* animation is starting and false if the animation has ended.
*/
synchronized public void addChangeListener(ChangeListener listener) {
if (listener == null)
return;
if (changeListeners == null)
changeListeners = new ArrayList