Home > database >  Animate needle transition
Animate needle transition

Time:03-23

When I read data from GPS sensor, it comes with a slight delay. You are not getting values like 0,1 0,2 0,3 0,4 0,5 etc, but they are coming like 1 then suddenly 5 or 9 or 12. In this case needle is jumping back and forth. Anybody have an idea how to make needle moving smoothly? I guess some kind of delay is needed?

Something like, taken from another control:

    async void animateProgress(int progress)
    {
        sweepAngle = 1;

        // Looping at data interval of 5
        for (int i = 0; i < progress; i=i 5)
        {
            sweepAngle = i;
            await Task.Delay(3);
        }
    }

However I am a bit confused how to implement that.

Here is code for drawing a needle on canvas:

    private void OnDrawNeedle()
    {
        using (var needlePath = new SKPath())
        {
            //first set up needle pointing towards 0 degrees (or 6 o'clock)
            var widthOffset = ScaleToSize(NeedleWidth / 2.0f);
            var needleOffset = ScaleToSize(NeedleOffset);
            var needleStart = _center.Y - needleOffset;
            var needleLength = ScaleToSize(NeedleLength);

            needlePath.MoveTo(_center.X - widthOffset, needleStart);
            needlePath.LineTo(_center.X   widthOffset, needleStart);
            needlePath.LineTo(_center.X, needleStart   needleLength);
            needlePath.LineTo(_center.X - widthOffset, needleStart);
            needlePath.Close();

            //then calculate needle position in degrees
            var needlePosition = StartAngle   ((Value - RangeStart) / (RangeEnd - RangeStart) * SweepAngle);

            //finally rotate needle to actual value
            needlePath.Transform(SKMatrix.CreateRotationDegrees(needlePosition, _center.X, _center.Y));

            using (var needlePaint = new SKPaint())
            {
                needlePaint.IsAntialias = true;
                needlePaint.Color = NeedleColor.ToSKColor();
                needlePaint.Style = SKPaintStyle.Fill;
                _canvas.DrawPath(needlePath, needlePaint);
            }
        }
    }

EDIT:

Still having hard times to understand the process.

Let's say I don't want to apply this filter for control but have it in ViewModel to filter value. I have a Class from where I am getting data, for example GPSTracker. GPSTracker provides speed value, then I am subscribing to EventListener in my HomeViewModel and want to filter incoming value.

Based on Adams answer:

CodePudding user response:

Coming from a controls background, to mimic behavior of an analog device, you could use an exponential (aka low-pass) filter.

There are two types of low-pass filters you can use, depending on what type of behavior you want to see: a first-order or second-order filter. To put it in a nutshell, if your reading was steady at 0 then suddenly changed to 10 and held steady at 10 (a step change), the first order would slowly go to 10, never passing it, then remain at 10 whereas the second order would speed up its progress towards 10, pass it, then oscillate in towards 10.

The function for an exponential filter is simple:

public void Exp_Filt(ref double filtered_value, double source_value, double time_passed, double time_constant)
{
    if (time_passed > 0.0)
    {
        if (time_constant > 0.0)
        {
            source_value  = (filtered_value - source_value) * Math.Exp(-time_passed / time_constant);
        }
        filtered_value = source_value;
    }
}

filtered_value is the filtered version of the source source_value, time_passed is how much time passed from the last time this function was called to filter filtered_value, and time_constant is the time constant of the filter (FYI, reacting to a step change, filtered_value will get 63% of the way towards source_value after time_constant time has passed and 99% when 5x have passed). The units of filtered_value will be the same as source_value. The units of time_passed and time_constant need to be the same, whether this be seconds, microseconds, or jiffy. Additionally, time_passed should be significantly smaller than time_constant at all times, otherwise the filter behavior will become non-deterministic. There are multiple ways to get the time_passed, such as Stopwatch, see How can I calculate how much time have been passed?

Before using the filter function, you would need to initialize the filtered_value and whatever you use to get time_passed. For this example, I will use stopwatch.

var stopwatch = new System.Diagnostics.Stopwatch();
double filtered_value, filtered_dot_value;
...
filtered_value = source_value;
filtered_dot_value = 0.0;
stopwatch.Start();

To use this function for a first-order filter, you would loop the following using a timer or something similar

double time_passed = stopwatch.ElapsedMilliseconds;
stopwatch.Restart();
Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);

To use this function for a second-order filter, you would loop the following using a timer or something similar

double time_passed = stopwatch.ElapsedMilliseconds;
stopwatch.Restart();
if (time_passed > 0.0)
{
    double last_value = filtered_value;
    filtered_value  = filtered_dot_value * time_passed;
    Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
    Exp_Filt(ref filtered_dot_value, (filtered_value - last_value) / time_passed, time_passed, dot_time_constant);
}

The second-order filter works by taking the first derivative of the first-order filtered value into account. Also, I would recommend making time_constant < dot_time_constant - to start, I would set dot_time_constant = 2 * time_constant

Personally, I would call this filter in a background thread controlled by a threading timer and have time_passed a constant equal to the timer's period, but I will leave the implementation specifics up to you.

EDIT:

Below is example class to create first and second order filters. To operate the filter, I use a threading timer set to process every 100 milliseconds. Being that this timer is rather consistent, making time_passed constant, I optimized the filter equation by pre-calculating Math.Exp(-time_passed / time_constant) and not dividing/multiplying 'dot' term by time_passed.

For first-order filter, use var filter = new ExpFilter(initial_value, time_constant). For second-order filter, use var filter = new ExpFilter(initial_value, time_constant, dot_time_constant). Then, to read latest filtered value, call double value = filter.Value. To set value to filter towards, call filter.Value = value.

    public class ExpFilter : IDisposable
    {
        private double _input, _output, _dot;
        private readonly double _tc, _tc_dot;
        private System.Threading.Timer _timer;

        /// <summary>
        /// Initializes first-order filter
        /// </summary>
        /// <param name="value">initial value of filter</param>
        /// <param name="time_constant">time constant of filter, in seconds</param>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="time_constant"/> must be positive</exception>
        public ExpFilter(double value, double time_constant)
        {
            // time constant must be positive
            if (time_constant <= 0.0) throw new ArgumentOutOfRangeException(nameof(time_constant));

            // initialize filter
            _output = _input = value;
            _dot = 0.0;

            // calculate gain from time constant
            _tc = CalcTC(time_constant);

            // disable second-order
            _tc_dot = -1.0;

            // start filter timer
            StartTimer();
        }

        /// <summary>
        /// Initializes second-order filter
        /// </summary>
        /// <param name="value">initial value of filter</param>
        /// <param name="time_constant">time constant of primary filter, in seconds</param>
        /// <param name="dot_time_constant">time constant of secondary filter, in seconds</param>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="time_constant"/> and <paramref name="dot_time_constant"/> must be positive</exception>
        public ExpFilter(double value, double time_constant, double dot_time_constant)
        {
            // time constant must be positive
            if (time_constant <= 0.0) throw new ArgumentOutOfRangeException(nameof(time_constant));
            if (dot_time_constant <= 0.0) throw new ArgumentOutOfRangeException(nameof(dot_time_constant));

            // initialize filter
            _output = _input = value;
            _dot = 0.0;

            // calculate gains from time constants
            _tc = CalcTC(time_constant);
            _tc_dot = CalcTC(dot_time_constant);

            // start filter timer
            StartTimer();
        }

        // the following two functions must share the same time period
        private double CalcTC(double time_constant)
        {
            // time period = 0.1 s (100 ms)
            return Math.Exp(-0.1 / time_constant);
        }
        private void StartTimer()
        {
            // time period = 100 ms
            _timer = new System.Threading.Timer(Filter_Timer, this, 100, 100);
        }

        ~ExpFilter()
        {
            Dispose(false);
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                _timer.Dispose();
            }
        }

        /// <summary>
        /// Get/Set filter value
        /// </summary>
        public double Value
        {
            get => _output;
            set => _input = value;
        }

        private static void Filter_Timer(object stateInfo)
        {
            var _filter = (ExpFilter)stateInfo;

            // get values
            double _input = _filter._input;
            double _output = _filter._output;
            double _dot = _filter._dot;

            // if second-order, adjust _output (no change if first-order as _dot = 0)
            // then use filter function to calculate new filter value
            _input  = (_output   _dot - _input) * _filter._tc;
            _filter._output = _input;

            if (_filter._tc_dot >= 0.0)
            {
                // calculate second-order portion of filter
                _output = _input - _output;
                _output  = (_dot - _output) * _filter._tc_dot;
                _filter._dot = _output;
            }
        }
    }
  • Related