classdef AnimationTimer < ...
        matlab.graphics.primitive.world.SceneNode & ...
        matlab.graphics.mixin.UIParentable
%AnimationTimer Construct the timer controlling animation.
%   T = AnimationTimer
%   returns the animation timer of the current figure. If no such timer
%   exists, it is created.
%
%   T = AnimationTimer(fig)
%   returns the animation timer of the figure fig.
%
%   T = AnimationTimer(..., 'TimeBegin', VAL) sets the start time. Default is 0.
%
%   T = AnimationTimer(..., 'TimeEnd', VAL) sets the end time. Default is 10.
%
%   T = AnimationTimer(..., 'TimeStep', VAL) sets the length of the time interval 
%   in seconds between updates of the animated objects. Default is 0.1.
%
%   T = AnimationTimer(..., 'Backwards', true) makes the timer run backwards.
%
%   T = AnimationTimer(..., 'SpeedFactor', VAL) determines that the animation parameter
%   is increased by VAL per second. Default is 1.

%   Copyright 2018 The MathWorks, Inc.

properties(Constant)
   Type = 'animationTimer';
end

properties (SetAccess = 'immutable')
   Timer;
end

properties
   TimeBegin = 0;
   TimeEnd = 10;
   TimeStep = 0.1;
   CurrentTime;
   SpeedFactor = 1;
end


methods
    function self = AnimationTimer(varargin)
       % constructor of class AnimationTimer
       p = inputParser;
       addOptional(p, 'figure', get(groot, 'CurrentFigure'), @isgraphics);
       addParameter(p, 'TimeBegin', NaN, @isnumeric);
       addParameter(p, 'TimeEnd', NaN, @isnumeric);
       addParameter(p, 'TimeStep', NaN, @isnumeric);
       addParameter(p, 'Backwards', false, @islogical);
       addParameter(p, 'SpeedFactor', NaN, @isreal);
       parse(p, varargin{:});
       F = p.Results.figure;
       if isempty(F)
           F = gcf;
       end
       existingTimer = findAnimationTimer(F);
       timerExists = ~isempty(existingTimer);
       
       if timerExists
           self = existingTimer;
       else
           self.Parent = F;
       end
       
       for fieldname = ["TimeBegin", "TimeEnd", "TimeStep", "SpeedFactor"]
          % if a name-value pair is explicitly given, overwrite field
          fn = p.Results.(fieldname);
          if ~isnan(fn)
            self.(fieldname) = fn;
          end
       end    
       
       % Timers cannot have a period < 1/1000, 
       % and the period has to be a multiple of 1/1000
       self.TimeStep = max(round(self.TimeStep, 3), 1/1000); 
       
       if p.Results.Backwards 
           self.SpeedFactor = -self.SpeedFactor;
       end
       
       if ~timerExists || ~isvalid(self.Timer)
           % We have to initialize the internal timer
           self.Timer = timer('ExecutionMode', 'fixedSpacing', ...
               'TasksToExecute', 9e18,...  % do not use inf - we use wait 
               'Period', self.TimeStep, ...
               'TimerFcn', @(~,~) self.timeStep);
       else
           self.Timer.Period = self.TimeStep;
       end
       rewind(self);
    end % constructor

    function delete(self)
        delete(self.Timer);
    end % destructor     

    function timeStep(self)
        % Avoid using nested functions for the timer callback because
        % it causes the timer to serialize the local workspace, which
        % includes the figure (F). This causes the live editor snapshotting
        % code to leak this figure when calling WorkspaceUtilities.deserializeArray
        setTime(self, self.CurrentTime + self.SpeedFactor * self.TimeStep);
    end

    function start(self)
        % do nothing if the timer is running anyway
        if strcmp(self.Timer.Running, 'off')
            if self.CurrentTime >= self.TimeEnd || ...
                    self.CurrentTime <= self.TimeBegin
                rewind(self);
            end
            start(self.Timer);
        end
    end

    function pause(self)
        stop(self.Timer);
    end

    function rewind(self)
       if self.SpeedFactor > 0 
           setTime(self, self.TimeBegin); 
       else
           setTime(self, self.TimeEnd);
       end    
    end

    function setTime(self, t)
        if t > self.TimeEnd
            self.CurrentTime = self.TimeEnd;
            pause(self);
        elseif t < self.TimeBegin
            self.CurrentTime = self.TimeBegin;
            pause(self);
        else
            self.CurrentTime = t;
        end

        F = self.Parent;
        animators = findobj(F, '-class', 'matlab.graphics.function.Animator'); 
        if ~isempty(animators)
            setTime(animators, self.CurrentTime);
        end    
    end

end % methods

end % classdef