/* 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 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 ArrayList changeListeners; /** * The animation consists of running this method. To create an animation, it is generally * only necessary to implement this method. This method should call pause * 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. *

(Note that if some error other than an 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. *

In addition to checking for cancellation, this method also checks to see whether the * animation has been paused by calling its 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(); changeListeners.add(listener); } /** * Remove a ChangeListener from the animation, if it is currently registered as a listener. */ synchronized public void removeChangeListener(ChangeListener listener) { if (listener != null && changeListeners != null) { changeListeners.remove(listener); if (changeListeners.isEmpty()) changeListeners = null; } } /** * Sends a ChangeEvent to any registered ChangeListeners. This is not likely to be used in a subclass, * unless the subclass wants to send additional change events besides those sent when the * animation starts and stops. */ synchronized protected void fireAnimationChangeEvent() { if (changeListeners == null) return; if (changeEvent == null) changeEvent = new ChangeEvent(this); for (int i = 0; i < changeListeners.size(); i++) changeListeners.get(i).stateChanged(changeEvent); } // -------------------------- status bar text ------------------------- /** * Returns null to indicate that the default text ("Animation Running" or "Animation Paused" in the * English version) should be shown in the display's status bar. This can be overridden in * a subclass to show a different status message. * @param running tells whether the animation is currently running. */ public String getStatusText(boolean running) { return null; } }