package org.geogebra.common.kernel;
import java.util.ArrayList;
import java.util.TreeSet;
import org.geogebra.common.kernel.algos.AlgoElement;
import org.geogebra.common.kernel.geos.Animatable;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.kernelND.GeoElementND;
import org.geogebra.common.util.GTimer;
import org.geogebra.common.util.GTimerListener;
/**
* Updates all animated geos based on slider ticks
*/
public class AnimationManager implements GTimerListener {
/** animation time */
public final static int STANDARD_ANIMATION_TIME = 10; // secs
/** max frames per second */
public final static int MAX_ANIMATION_FRAME_RATE = 30; // frames per second
/** min frames per second */
public final static int MIN_ANIMATION_FRAME_RATE = 2; // frames per second
/** kernel */
protected Kernel kernel;
/** animated geos */
protected ArrayList<GeoElement> animatedGeos;
/** changed geos */
protected ArrayList<GeoElementND> changedGeos;
/** current frame rate */
protected double frameRate = MAX_ANIMATION_FRAME_RATE;
private boolean needToShowAnimationButton;
/**
* list containing all {@link TimerListener} that will receive notifications
* when the timer is started or stopped
*/
protected ArrayList<TimerListener> listener = new ArrayList<TimerListener>();
private GTimer timer;
/**
* @param kernel2
* kernel
*/
public AnimationManager(Kernel kernel2) {
this.kernel = kernel2;
animatedGeos = new ArrayList<GeoElement>();
changedGeos = new ArrayList<GeoElementND>();
timer = kernel.getApplication().newTimer(this,
1000 / MAX_ANIMATION_FRAME_RATE);
}
/**
* Returns whether the animation button needs to be drawn in the graphics
* view. This is only needed when there are animated geos with non-dynamic
* speed.
*
* @return true if we need to draw animation button
*/
final public boolean needToShowAnimationButton() {
return needToShowAnimationButton;
}
/**
* Updates the needToShowAnimationButton value.
*/
public void updateNeedToShowAnimationButton() {
int size = animatedGeos.size();
if (size == 0) {
needToShowAnimationButton = false;
return;
}
// if one animated geo has a static speed, we need to get out of here
for (int i = 0; i < size; i++) {
GeoElement geo = animatedGeos.get(i);
GeoElement animObj = geo.getAnimationSpeedObject();
if (animObj == null
|| !animObj.isLabelSet() && animObj.isIndependent()) {
needToShowAnimationButton = true;
return;
}
}
// all animated geos have dynamic speed
needToShowAnimationButton = false;
}
/**
* Adds geo to the list of animated GeoElements.
*
* @param geo
* the GeoElement to add
*/
final public synchronized void addAnimatedGeo(GeoElement geo) {
if (geo.isAnimating() && !animatedGeos.contains(geo)) {
animatedGeos.add(geo);
// if (animatedGeos.size() == 1) removed, might have geos with
// variable controlling speed
updateNeedToShowAnimationButton();
}
}
/**
* Removes geo from the list of animated GeoElements.
*
* @param geo
* the GeoElement to remove
*/
final public synchronized void removeAnimatedGeo(GeoElement geo) {
if (animatedGeos.remove(geo) && animatedGeos.size() == 0) {
stopAnimation();
}
updateNeedToShowAnimationButton(); // added, might have geos with
// variable controlling speed
}
/**
* Starts animation
*/
public synchronized void startAnimation() {
if (kernel.getApplication().isScreenshotGenerator()) {
return;
}
if (!isRunning() && animatedGeos.size() > 0) {
updateNeedToShowAnimationButton();
startTimer();
}
}
/**
* Stops animation
*/
public synchronized void stopAnimation() {
if (isRunning()) {
stopTimer();
updateNeedToShowAnimationButton();
}
}
/**
* Returns whether the animation is currently paused, i.e. the animation is
* not running but there are elements with "Animation on" set.
*
* @return true when paused
*/
public boolean isPaused() {
return !isRunning() && animatedGeos.size() > 0;
}
/**
* Empties list of animated geos
*/
public void clearAnimatedGeos() {
for (int i = 0; i < animatedGeos.size(); i++) {
GeoElement geo = animatedGeos.get(i);
geo.setAnimating(false);
}
animatedGeos.clear();
updateNeedToShowAnimationButton();
}
/**
* Adapts the frame rate depending on how long it took to compute the last
* frame.
*
* @param frameTime
*/
private void adaptFrameRate(long compTime) {
// only allow to use 80% of CPU time for animation (800 millis out of 1
// sec)
double framesPossible = 800.0 / compTime;
// the frameRate is too high: decrease it
if (framesPossible < frameRate) {
frameRate = Math.max(framesPossible, MIN_ANIMATION_FRAME_RATE);
setTimerDelay((int) Math.round(1000.0 / frameRate));
// System.out.println("DECREASED frame rate: " + frameRate +
// ", framesPossible: " + framesPossible);
}
// the frameRate is too low: try to increase it
else if (frameRate < MAX_ANIMATION_FRAME_RATE) {
frameRate = Math.min(framesPossible, MAX_ANIMATION_FRAME_RATE);
setTimerDelay((int) Math.round(1000.0 / frameRate));
// System.out.println("INCREASED frame rate: " + frameRate +
// ", framesPossible: " + framesPossible);
}
}
private TreeSet<AlgoElement> tempSet;
private TreeSet<AlgoElement> getTempSet() {
if (tempSet == null) {
tempSet = new TreeSet<AlgoElement>();
}
return tempSet;
}
/**
* Perform one step
*/
protected void sliderStep() {
// skip animation frames while kernel is saving XML
if (kernel.isSaving()) {
return;
}
kernel.notifyBatchUpdate();
long startTime = System.currentTimeMillis();
// clear list of geos that need to be updated
changedGeos.clear();
// perform animation step for all animatedGeos
// go right to left to ensure removing geos animated once does not kill
// this #4193
int size = animatedGeos.size();
for (int i = size - 1; i >= 0; i--) {
Animatable anim = (Animatable) animatedGeos.get(i);
GeoElementND changed = anim.doAnimationStep(frameRate, null);
if (changed != null) {
changedGeos.add(changed);
}
}
// do we need to update anything?
if (changedGeos.size() > 0) {
// efficiently update all changed GeoElements
GeoElement.updateCascade(changedGeos, getTempSet(), false);
// repaint views
kernel.notifyRepaint();
// check frame rate
long compTime = System.currentTimeMillis() - startTime;
if (kernel.getApplication().getEuclidianView1() != null) {
compTime += kernel.getApplication().getEuclidianView1()
.getLastRepaintTime();
}
if (kernel.getApplication().hasEuclidianView2(1)) {
compTime += kernel.getApplication().getEuclidianView2(1)
.getLastRepaintTime();
}
adaptFrameRate(compTime);
// System.out.println("UPDATE compTime: " + compTime +
// ", frameRate: " + frameRate);
// collect some potential garbage
kernel.notifyRemoveGroup();
}
kernel.notifyEndBatchUpdate();
}
/**
* add a {@link TimerListener} that will be notified when the timer is
* started or stopped
*
* @param timerListener
* the listener to be added
*/
public void addListener(TimerListener timerListener) {
listener.add(timerListener);
}
/**
* removes a {@link TimerListener} that will no longer receive notifications
* when the timer is started or stopped
*
* if there exists more than one {@link TimerListener} that is equal to the
* given listener (e.g. if one listener was added multiple times), only the
* first one will be removed
*
* @param timerListener
* the listener to be removed
*/
public void removeTimerListener(TimerListener timerListener) {
listener.remove(timerListener);
}
/**
* @return whether the animation is currently running.
*/
public boolean isRunning() {
return timer.isRunning();
}
/**
* @param i
* delay in miliseconds
*/
protected void setTimerDelay(int i) {
timer.setDelay(i);
}
@Override
public void onRun() {
sliderStep();
}
/**
* stops timer
*/
protected void stopTimer() {
timer.stop();
for (TimerListener tl : listener) {
tl.onTimerStopped();
}
}
/**
* starts timer
*/
protected void startTimer() {
timer.startRepeat();
for (TimerListener tl : listener) {
tl.onTimerStarted();
}
}
/**
* current frame rate
*
* @return in seconds
*/
public double getFrameRate() {
return frameRate;
}
}