1

I am working on a project that plotting the mouse tracking. The MouseInfo class is defined like:

public class MouseInfo {
    public readonly long TimeStamp;
    public readonly int PosX;
    public readonly int PosY;
    public int ButtonsDownFlag;
}

I need to find a way to extract the mouse positions from a List<MouseInfo> which ButtonsDownFlag has at least 2 continuous 1s and group them together, so that I can distinguish clicks and draggings, which will then being used for plotting.

The current way I am doing is to iterate through the list, and add the found values one by one to other lists, which is very slow, expensive and the code looks messy. I wonder if there is any more elegant way to do it? Will Linq help?

For example, I have the recording of below:

(t1, x1, y1, 0)
(t2, x2, y2, 1)
(t3, x3, y3, 1)
(t4, x4, y4, 0)
(t5, x5, y5, 1)
(t6, x6, y6, 0)
(t7, x7, y7, 1)
(t8, x8, y8, 1)
(t9, x9, y9, 1)
(ta, xa, ya, 0)
(tb, xb, yb, 2) <- Yes, ButtonDownFlag can be 2 for RightClicks or even 3 for both buttons are down
(tc, xc, yc, 0)
(td, xd, yd, 2)
(te, xe, ye, 2)

I want two Lists (or similiar presentation) which are

((t2, x2, y2), (t2, x3, y3), (t7, x7, y7), (t7, x8, y8), (t7, x9, y9))

and

((x5, y5, 1), (xb, yb, 2), (xd, yd, 2), (xe, ye, 2))

Note:

  1. In the first list, I need TimeStamp in the subsequence elements being altered to the first element's TimeStamp, so that I can group in later plotting.
  2. In the second list, I don't care TimeStamp but I do care the ButtonDownFlag
  3. I don't mind if ButtonDownFlag exists in the first list, nor TimeStamp exists in the second list.
  4. Continuous "Right Clicks" are treated as separate "Right Clicks" rather than "Right dragging".
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
Tide Gu
  • 815
  • 1
  • 7
  • 21
  • For now you are doing Something like : `foreach(){list1.Add(new myObject(x,y,z) ); list2.Add(new myObject(x,y,z) ); }` ? – Drag and Drop Jan 03 '18 at 12:20
  • @DragandDrop Yes, similiar to this. – Tide Gu Jan 03 '18 at 12:23
  • List size aproximatly? – Drag and Drop Jan 03 '18 at 12:24
  • Perhaps you have issue with the reallocation on the list try to init the list to var list = new List(OriginalDataCount); – Drag and Drop Jan 03 '18 at 12:27
  • @DragandDrop It depends. Most recordings are less than 50k elements, some are more. But it should be normally less than 200k elements. – Tide Gu Jan 03 '18 at 12:31
  • Declare your list with the correct size, it may fix the performance issue https://stackoverflow.com/questions/5176916/adding-to-listt-becomes-very-slow-over-time – Drag and Drop Jan 03 '18 at 12:32
  • @DragandDrop Thanks. Any idea if there's any more elegant way? Looping through a list is the last thing I would like, unless it's the only option... – Tide Gu Jan 03 '18 at 12:42
  • 1
    Form the performance side I must tell you that Linq will iterate throught list like a foreach will. The only diffenrece is that the execution will be delay till the result is needed. For performance : http://www.schnieds.com/2009/03/linq-vs-foreach-vs-for-loop-performance.html. – Drag and Drop Jan 03 '18 at 12:46
  • for elegant and pretty code I don't see your code but the exemple i gave in first comment is pretty clear. But with linq you can try to bost the perf with a IEnumerable.AsParallel().ForAll() . But try to limit the parallel core. – Drag and Drop Jan 03 '18 at 12:50
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/162421/discussion-between-drag-and-drop-and-tide-gu). – Drag and Drop Jan 03 '18 at 12:52
  • https://stackoverflow.com/questions/14879197/linq-query-data-aggregation-group-adjacent – Slai Jan 03 '18 at 13:46

3 Answers3

1

There is a means by which you can use LINQ to do this which will produce one list for all events which are part of a drag sequence and a separate list for individual click events.

            List<MouseInfo> mainList = new List<MouseInfo>();

            //populate mainList with some data...

            List<MouseInfo> dragList = mainList.Where
                (
                // check if the left click is pressed
                    x => x.ButtonsDownFlag == 1

                    //then check if either the previous or the following elements are also clicked
                    &&
                    (
                        //if this isnt the first element in the list, check the previous one
                        (mainList.IndexOf(x) != 0 ? mainList[mainList.IndexOf(x) - 1].ButtonsDownFlag == 1 : false)

                        //if this isnt the last element in the list, check the next one
                        || (mainList.IndexOf(x) != (mainList.Count - 1) ? mainList[mainList.IndexOf(x) + 1].ButtonsDownFlag == 1 : false)
                    )
                ).ToList();

            List<MouseInfo> clickList = mainList.Where
                (
                    // check if the left/right or both click is pressed
                    x => (x.ButtonsDownFlag == 1 || x.ButtonsDownFlag == 2 || x.ButtonsDownFlag == 3)

                    //then make sure that neither of the previous or following elements are also clicked
                    &&
                    (mainList.IndexOf(x) != 0 ? mainList[mainList.IndexOf(x) - 1].ButtonsDownFlag != 1 : true)

                    &&                 
                    (mainList.IndexOf(x) != (mainList.Count - 1) ? mainList[mainList.IndexOf(x) + 1].ButtonsDownFlag != 1 : true)

                ).ToList();

This approach does have the limitation of not "labelling" each sequence of drags with the same timestamp.

An alternative would be to do this logic at point of data capture. When each data point is captured, if it has a "ButtonDown" value, check the previous data point. If that data point is also a "ButtonDown" add them both (or however many you end up with in the sequence) to your "dragList", otherwise add it to the "clickList". For this option I would also be tempted to add some logic to separate out your different drag sequences. You have done this by changing the time stamp of the subsequent points, I would instead be tempted to create your "dragList" as a dictionary instead. With each sequences of drags put into a different distinct key.

Paul Marshall
  • 523
  • 4
  • 11
1

I don't think this is too easy to follow, but it is similar to how you might handle this in APL (I used Excel to work it out). I also won't promise how fast this is - generally foreach is faster than LINQ, even if only by a small amount.

Using extension methods to implement APL's scan and compress operators and to append/prepend to IEnumerables:

public static IEnumerable<TResult> Scan<T, TResult>(this IEnumerable<T> src, TResult seed, Func<TResult, T, TResult> combine) {
    foreach (var s in src) {
        seed = combine(seed, s);
        yield return seed;
    }
}
public static IEnumerable<T> Compress<T>(this IEnumerable<bool> bv, IEnumerable<T> src) {
    var srce = src.GetEnumerator();
    foreach (var b in bv) {
        srce.MoveNext();
        if (b)
            yield return srce.Current;
    }
}
public static IEnumerable<T> Prepend<T>(this IEnumerable<T> rest, params T[] first) => first.Concat(rest);
public static IEnumerable<T> Append<T>(this IEnumerable<T> rest, params T[] last) => rest.Concat(last);

You can filter the list to groups of drags and what's not in a drag:

// create a terminal MouseInfo for scanning along the moves
var mterm = new MouseInfo { t = 0, x = 0, y = 0, b = 4 };
// find the drags for button 1 except the first row
var bRestOfDrag1s = moves.Append(mterm).Zip(moves.Prepend(mterm), (dm, em) => dm.b == 1 && dm.b == em.b).ToList();
// number the drags by finding the drag beginnings
var iLastDragNums = bRestOfDrag1s.Zip(bRestOfDrag1s.Skip(1), (fm, gm) => (!fm && gm)).Scan(0, (a, hm) => hm ? a + 1 : a).ToList();
// find the drags
var bInDrag1s = bRestOfDrag1s.Zip(bRestOfDrag1s.Skip(1), (fm, gm) => (fm || gm));
// number each drag row by its drag number
var dnmiDrags = bInDrag1s.Compress(Enumerable.Range(0, moves.Count)).Select(idx => new { DragNum = iLastDragNums[idx], mi = moves[idx] });
// group by drag number and smear first timestamp along drags
var drags = dnmiDrags.GroupBy(dnmi => dnmi.DragNum)
                     .Select(dnmig => dnmig.Select(dnmi => dnmi.mi).Select(mi => new MouseInfo { t = dnmig.First().mi.t, x = mi.x, y = mi.y, b = mi.b }).ToList()).ToList();

var clicks = bInDrag1s.Select(b => !b).Compress(moves).Where(mi => mi.b != 0).ToList();

When done, drags contains a List<List<MouseInfo>> where each sub-list is a drag. You can use SelectMany instead of the last (outside) Select to get just a flat List<MouseInfo> instead. clicks will contain a List<MouseInfo> with just the clicks.

Note that I abbreviated the MouseInfo field names.

BTW, using a for loop is considerably faster:

var inDrag = false;
var drags = new List<MouseInfo>();
var clicks = new List<MouseInfo>();
var beginTime = 0L;
for (var i = 0; i < moves.Count; ++i) {
    var curMove = moves[i];
    var wasDrag = inDrag;
    inDrag = curMove.b == 1 && (inDrag || (i + 1 < moves.Count ? moves[i + 1].b == 1 : false));
    if (inDrag) {
        if (wasDrag)
            drags.Add(new MouseInfo { t = beginTime, x = curMove.x, y = curMove.y, b = curMove.b });
        else {
            drags.Add(curMove);
            beginTime = curMove.t;
        }
    }
    else {
        if (curMove.b != 0)
            clicks.Add(curMove);
    }
}
NetMage
  • 26,163
  • 3
  • 34
  • 55
0

Just trying to share some knowledge - I found GroupAdjacent solved my problem very well (along with some tweeks for the plotting in a later stage).

The performance is surely not the best (compare to for loop) but I feel the code is more elegant!

Reference: https://blogs.msdn.microsoft.com/ericwhite/2008/04/20/the-groupadjacent-extension-method/

public static class LocalExtensions {
    public static IEnumerable<IGrouping<TKey, TSource>> GroupAdjacent<TSource, TKey>(
        this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector) {
        TKey last = default(TKey);
        bool haveLast = false;
        List<TSource> list = new List<TSource>();
        foreach (TSource s in source) {
            TKey k = keySelector(s);
            if (haveLast) {
                if (!k.Equals(last)) {
                    yield return new GroupOfAdjacent<TSource, TKey>(list, last);
                    list = new List<TSource>();
                    list.Add(s);
                    last = k;
                } else {
                    list.Add(s);
                    last = k;
                }
            } else {
                list.Add(s);
                last = k;
                haveLast = true;
            }
        }
        if (haveLast)
            yield return new GroupOfAdjacent<TSource, TKey>(list, last);
    }
}

class GroupOfAdjacent<TSource, TKey> : IEnumerable<TSource>, IGrouping<TKey, TSource> {
    public TKey Key { get; set; }
    private List<TSource> GroupList { get; set; }
    IEnumerator IEnumerable.GetEnumerator() {
        return ((IEnumerable<TSource>)this).GetEnumerator();
    }
    IEnumerator<TSource> IEnumerable<TSource>.GetEnumerator() {
        foreach (TSource s in GroupList)
            yield return s;
    }
    public GroupOfAdjacent(List<TSource> source, TKey key) {
        GroupList = source;
        Key = key;
    }
}

And my working code for testing:

private class MouseInfo {
    public readonly long TimeStamp;
    public readonly int PosX;
    public readonly int PosY;
    public int ButtonsDownFlag;
    public MouseInfo(long t, int x, int y, int flag) {
        TimeStamp = t;
        PosX = x;
        PosY = y;
        ButtonsDownFlag = flag;
    }
    public override string ToString() {
        return $"({TimeStamp:D2}: {PosX:D3}, {PosY:D4}, {ButtonsDownFlag})";
    }
}

public Program() {
    List<MouseInfo> mi = new List<MouseInfo>(14);
    mi.Add(new MouseInfo(1, 10, 100, 0));
    mi.Add(new MouseInfo(2, 20, 200, 1));
    mi.Add(new MouseInfo(3, 30, 300, 1));
    mi.Add(new MouseInfo(4, 40, 400, 0));
    mi.Add(new MouseInfo(5, 50, 500, 1));
    mi.Add(new MouseInfo(6, 60, 600, 0));
    mi.Add(new MouseInfo(7, 70, 700, 1));
    mi.Add(new MouseInfo(8, 80, 800, 1));
    mi.Add(new MouseInfo(9, 90, 900, 1));
    mi.Add(new MouseInfo(10, 100, 1000, 0));
    mi.Add(new MouseInfo(11, 110, 1100, 2));
    mi.Add(new MouseInfo(12, 120, 1200, 0));
    mi.Add(new MouseInfo(13, 130, 1300, 2));
    mi.Add(new MouseInfo(14, 140, 1400, 2));

    var groups = mi.GroupAdjacent(x => x.ButtonsDownFlag);

    List<List<MouseInfo>> drags = groups.Where(x => x.Key == 1 && x.Count() > 1).Select(x => x.ToList()).ToList();
    foreach (var d in drags) 
        foreach (var item in d)
            Console.Write($"{item} ");
    Console.WriteLine();

    List<List<MouseInfo>> clicks = groups.Where(x => x.Key > 1 || (x.Key == 1 && x.Count() == 1)).Select(x => x.ToList()).ToList();
    foreach (var d in clicks) {
        foreach (var item in d)
            Console.Write($"{item} ");
        Console.WriteLine();
    }
}

[MTAThread]
static void Main(string[] args) {
    var x = new Program();
    Console.ReadLine();
    return;
}
Tide Gu
  • 815
  • 1
  • 7
  • 21