AbstractTimer.java

/*
 *                      ..::jDrawingLib::..
 *
 * Copyright (C) Federico Vera 2012 - 2023 <[email protected]>
 *
 * This program 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 3 of the License, or any later
 * version.
 *
 * This program 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 program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.dkt.graphics.extras.formula;

import com.dkt.graphics.elements.GPoint;
import com.dkt.graphics.elements.GPointArray;
import com.dkt.graphics.elements.Graphic;
import com.dkt.graphics.elements.GraphicE;
import com.dkt.graphics.exceptions.AlreadyRunningException;
import com.dkt.graphics.exceptions.InvalidArgumentException;
import com.dkt.graphics.utils.PThread;
import java.util.ArrayList;

/**
 * This class implements an abstract timer mainly used to draw functions on
 * the canvas.<br>
 * Please note that there are several ways to improve the speed of this class,
 * for instance using a {@link Graphic} object for each thread, using
 * {@link GPointArray} instead of an array of {@link GPoint}s, etc. Those
 * things will improve the speed and memory quite a bit and the change is almost
 * trivial... <i>Why don't we improve it? </i> well it's quite simple actually,
 * we use this classes to draw the functions as a <i>real time</i> drawing, and
 * in that case, the perceived speed of the application is somewhat more
 * important than the real speed.
 *
 * @author Federico Vera {@literal<[email protected]>}
 * @param <T> The {@link AbstractCalculable} instance that will be used
 */
public abstract class AbstractTimer<T extends AbstractCalculable> extends GraphicE {
    private final ArrayList<PThread> threads = new ArrayList<>(1);
    private final GPointArray pointArray = new GPointArray();
    private final T calculable;
    private volatile boolean isRunning;
    private volatile boolean isPaused;
    private int numberOfThreads = 1;
    private boolean drawAsPath;
    private boolean drawPen;
    private Action action;
    
    /**
     * This interface contains all the methods that will be executed
     * after starting, pausing, resuming and stopping a timer.
     */
    public interface Action {
        /**
         * This method will be executed immediately after the timer starts and
         * all threads are created and running.
         */
        void start();
        /**
         * This method will be executed immediately after all the timer threads
         * are paused.
         */
        void pause();
        /**
         * This method will be executed immediately after all the timer threads
         * were resumed.
         */
        void resume();
        /**
         * This method will be executed immediately after all the timer threads
         * were stopped.<br>
         * <i>Note: </i> is the responsibility of the coder to call
         * {@link AbstractTimer#stop()} at the end of the execution of the
         * timer's instance
         */
        void stop();
    }

    /**
     * @param calculable object that will be used on the calculations
     * @throws IllegalArgumentException if {@code calculable} is {@code null}
     */
    protected AbstractTimer(T calculable){
        if (calculable == null){
            throw new IllegalArgumentException("You must pass a formula!");
        }

        this.calculable = calculable;
    }

    /**
     * Retrieves the calculable object used for the calculations.
     *
     * @return {@link AbstractCalculable} object used to calculate
     */
    public T getCalculable(){
        return calculable;
    }

    /**
     * Sets the number of threads that will be used for this calculations
     *
     * @param n number of threads
     * @throws InvalidArgumentException if {@code n} is less than one
     * @throws AlreadyRunningException if the Timer was already started
     */
    public void setNumberOfThreads(int n){
        checkRunning();

        if (n <= 0){
            String msg = "You must have at least one thread";
            throw new InvalidArgumentException(msg);
        }

        numberOfThreads = n;
    }

    /**
     * Tells if all the threads have finished their execution
     *
     * @return {@code true} if there's no running or paused thread and
     * {@code false} otherwise
     */
    protected boolean threadsEnded() {
        synchronized (threads){
            return threads.isEmpty();
        }
    }

    /**
     * Tells the timer to draw the pen on the last drawn point
     *
     * @param drawPen {@code true} if the pen must be drawn, and {@code false}
     * otherwise
     * @throws AlreadyRunningException if the Timer was already started
     */
    public void setDrawPen(boolean drawPen){
        checkRunning();

        this.drawPen = drawPen;
    }

    /**
     * Tells the timer to draw the equation as a path o points
     *
     * @param drawAsPath {@code true} if the equation should be drawn as a path,
     * and {@code false} in order to draw only the points
     * @throws AlreadyRunningException if the Timer was already started
     */
    public void setDrawAsPath(boolean drawAsPath){
        checkRunning();

        this.drawAsPath = drawAsPath;
    }

    /**
     * Tells if the equation will be drawn as a path
     *
     * @return {@code true} if the equation is drawn as a path and {@code false}
     * if it's drawn as points
     */
    public boolean drawAsPath(){
        return drawAsPath;
    }

    /**
     * Tells if the timer is paused
     *
     * @return {@code true} if the thread is paused and {@code false} otherwise
     */
    public boolean isPaused(){
        return isPaused;
    }

    /**
     * Tells if the timer is running
     *
     * @return {@code true} if the thread is running and {@code false} otherwise
     */
    public boolean isRunning(){
        return isRunning;
    }

    /**
     * Starts the timer with all of its threads.<br>
     * <i>Note: </i> This method will remove all of the {@link GraphicE} of the
     * {@link Graphic}
     *
     * @throws AlreadyRunningException if the Timer was already started
     * @see Action#start()
     */
    public void start(){
        checkRunning();

        pointArray.clear();

        isRunning = true;

        synchronized(threads){
            for (int i = 0; i < numberOfThreads; i++){
                PThread thread = getThread(calculable, i, numberOfThreads, drawPen);
                threads.add(thread);
                thread.start();
            }

            if (action != null){
                action.start();
            }
        }
    }

    /**
     * Pauses the timer with all of it's threads
     *
     * @see Action#pause()
     */
    public void pause() {
        if (!isPaused){
            synchronized (threads){
                isPaused = true;

                for (PThread thread : threads){
                    thread.pause();
                }

                if (action != null){
                    action.pause();
                }
            }
        }
    }

    /**
     * Resumes the timer and all of it's threads
     *
     * @see Action#resume()
     */
    public void resume(){
        if (isPaused){
            synchronized (threads){
                isPaused = false;

                for (PThread thread : threads){
                    thread.unpause();
                }

                if (action != null){
                    action.resume();
                }
            }
        }
    }

    /**
     * Stops the timer and all of it's running threads
     *
     * @see Action#stop()
     */
    public void stop(){
        if (isRunning){
            isRunning = true;

            synchronized(threads){
                for (Thread thread : threads){
                    thread.interrupt();
                }

                threads.clear();
            }

            if (action != null){
                action.stop();
            }
        }

    }

    /**
     * Sets the actions to be executed at start, stop, pause, resume.
     *
     * @param action The actions to be executed
     * @see Action
     */
    public void setActions(Action action){
        checkRunning();
        this.action = action;
    }

    private void checkRunning() {
        if (isRunning){
            String msg = "The thread was already running";
            throw new AlreadyRunningException(msg);
        }
    }

    protected void removeThread(PThread thread){
        synchronized(threads){
            threads.remove(thread);
            if (threadsEnded()){
                stop();
            }
        }
    }

    protected abstract PThread getThread(T calculable,
                                         int threadNumber,
                                         int threadsTotal,
                                         boolean drawPen);
}