PlugIns/GenericEvents/VR-Applications: AutoQUESTGenericMonitorUnity.cs

File AutoQUESTGenericMonitorUnity.cs, 42.8 KB (added by pharms, 7 years ago)
Line 
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Security.Cryptography;
5using System.Text;
6using System.Threading;
7using UnityEngine;
8using UnityEngine.Events;
9using UnityEngine.EventSystems;
10using UnityEngine.SceneManagement;
11using UnityEngine.UI;
12
13/**
14 * This class can be used to monitor the usage of an AR/VR. For this, it needs to be set as a component of
15 * a game object in a scene. There is a prefab coming with the script, that contains only the script itself.
16 * The script automatically searches the scene for objects of type Selectable, ScrollRect, and EventTrigger
17 * and registers with them for any event that they handle. So for example, for any button in the scene, the
18 * script registers for PointerClick events. If any of the registered events occurs, the method LogEvent()
19 * is called with respective parameters being the event type and the target. The event type is a string
20 * denoting which event took place. The event target is the game object on which the event was observed.
21 * The events are collected until a number of 10 events were observed or the scene is ending. Then the
22 * collected events are sent in JSON format to a monitoring server, which stores them. The server URL can
23 * be configured using the public class variable serverURL.
24 * The events are always connected to an appId and a clientId. This is required by the server to identify
25 * which events belong to which running instance of an AR/VR. The app id is basically the name of the app.
26 * It can be changes per instance to allow event separation for different instances of this script. The
27 * client id is a pseudo id calculated based on environment variables of the system on which the app is
28 * running. It cannot be used to trace back the individual user.
29 * The logging can also be triggered in a static fashion from any other script upon request. For this, the
30 * first instance of this class in the scene graph can be retrieved using the getInstance() method. Then
31 * on this instance, LogEvent() can be called with the above mentioned parameters. A call of this method
32 * is handled in the same manner as the other calls caused by the self registration of this class for any
33 * scene internal events.
34 * The searching for all objects in a scene for which the usage must be monitored is done continuously in
35 * form of a coroutine. The reason for this is, that the objects in a scene may change over time and the
36 * script cannot be notified about new objects of interest. Hence, the coroutine performs a kind of polling
37 * mechanism. This mechanism is implemented in a fashion so that not too much action happens in a frame. In
38 * addition, the coroutine prevents to continuously check the full scene graph at once for objects of
39 * interest but only sub graphs starting at the different root nodes in a scene. This may cause, that some
40 * events on objects of interest may be lost. But this will not be the default case as the whole scene will
41 * be reconsidered in only a few frames and afterwards, the coroutine will wait only a further second
42 * before it starts rescanning the scene.
43 * In two further coroutines, the class continously traces the movement and rotation of the main camera
44 * which is considered to be the head of the AR/VR user. It must be done in coroutines as there is no
45 * notification possibility on location changes for the camera. And in addition, the check can be spread
46 * over time so that it takes place only once per 100 ms. If the coroutines determine a change in location
47 * or rotation, they produce corresponding events. Location and rotation changes usually take some time.
48 * Hence, they are logged, when they are finished. If in the meantime, other events happen, they are
49 * logged before that. But this is no drawback, as the location and rotation changes are logged together
50 * with time stamps indicating when the movement or rotation started and ended.
51 */
52public class AutoQUESTGenericMonitorUnity : MonoBehaviour
53{
54    /** The instance of this script which is considered the one to be returned when calling getInstance() */
55    private static AutoQUESTGenericMonitorUnity instance;
56
57    /** The id of the client, i.e., user, for which the actions are recorded (static, because it is the
58     *  same for all instances of this script) */
59    private static string clientId;
60
61    /** the registered event targets (static, because this map can become large and should, hence, be shared
62     *  between different instances of this script) */
63    private static Dictionary<GameObject, string> eventTargetIds = new Dictionary<GameObject, string>();
64
65    /** a lock for multiple reads but only single writes on the collected event target ids */
66    private static ReaderWriterLockSlim readWriteLockEventTargetIds = new ReaderWriterLockSlim();
67
68    /** the current list of events per server collected so far (static, because multiple instances of this
69     *  script may want to log to the same server and the events should always be sorted in the order in
70     *  which they occurred) */
71    private static Dictionary<string, List<EventData>> collectedEvents = new Dictionary<string, List<EventData>>();
72
73    /** a lock for multiple reads but only single writes on the collected event target ids */
74    private static ReaderWriterLockSlim readWriteLockCollectedEvents = new ReaderWriterLockSlim();
75
76    /** The AutoQUEST generic monitor server to send the logged data to (non static to allow for multiple
77     * script instances logging different things to different servers and to be configurable via unity) */
78    public string serverURL = "https://swe-tooling.informatik.uni-goettingen.de/autoquest-genericmonitor/";
79
80    /** The id of the application that the client uses (non static to allow for multiple script instances
81     *  logging different things for different virtual apps and to be configurable via unity) */
82    public string appId;
83
84    /**
85     * stores for any relevant game object the list of event listeners registered on it. This is required to
86     * remove them afterwards for clean up as removing can only be done for known listener objects
87     */
88    private Dictionary<UnityEngine.Object, Listeners> registeredUnityActions = new Dictionary<UnityEngine.Object, Listeners>();
89
90    /** determine the client id */
91    static AutoQUESTGenericMonitorUnity()
92    {
93        // determine a client id
94        StringBuilder id = new StringBuilder();
95        foreach (DictionaryEntry de in Environment.GetEnvironmentVariables())
96        {
97            id.Append(de.Key);
98            id.Append(de.Value);
99        }
100
101        clientId = toMD5Hash(id.ToString());
102    }
103
104    /**
105     * similar to a singleton, returns the single instance of this script in the scene. If there are multiple instances of this
106     * script, the first one found is taken. If there is not instance of this script attached to any active object in the scene
107     * an exception is thrown.
108     */
109    public static AutoQUESTGenericMonitorUnity getInstance()
110    {
111        readWriteLockCollectedEvents.EnterUpgradeableReadLock();
112
113        if (instance == null)
114        {
115            readWriteLockCollectedEvents.EnterWriteLock();
116            // search for the first instances of this script in active objects and take it as the single instance
117            instance = FindObjectOfType<AutoQUESTGenericMonitorUnity>();
118
119            if (instance == null)
120            {
121                // there is no instance of this script belonging to an active object in this scene. Therefore, we throw an
122                // exception, as this is a prerequisite
123                throw new Exception("getInstance() must not be called without an instance of this script attached to an " +
124                                    "active object in the scene. This is a programming mistake and must be corrected. You can, e.g., " +
125                                    "add the AutoQUEST Monitor Prefab to the scene to solve this problem.");
126            }
127
128            readWriteLockCollectedEvents.ExitWriteLock();
129        }
130
131        readWriteLockCollectedEvents.ExitUpgradeableReadLock();
132        return instance;
133    }
134
135    /**
136     * generates an app id if non is already set and starts all coroutines
137     */
138    void Start()
139    {
140        readWriteLockCollectedEvents.EnterWriteLock();
141
142        // get an id for the application
143        if ((appId == null) || ("".Equals(appId)))
144        {
145            appId = Application.productName;
146
147            if ((appId == null) || ("".Equals(appId)))
148            {
149                appId = Application.identifier;
150            }
151        }
152
153        StartCoroutine("EventHandlingRegistrationCoroutine");
154        StartCoroutine("MovementTrackingCoroutine");
155        StartCoroutine("GazeTrackingCoroutine");
156
157        readWriteLockCollectedEvents.ExitWriteLock();
158    }
159
160    /**
161     * Sends all remaining collected events to the server
162     */
163    void OnApplicationQuit()
164    {
165        StopAllCoroutines();
166        SendEventsToServer(true);
167    }
168
169    /**
170     * collects an event for a given game object and triggers sending events to the server, if sufficient events have been collected
171     */
172    public void LogEvent(string eventType, GameObject eventTarget, params KeyValuePair<String, String>[] parameters)
173    {
174        // determine the target
175        RegisterEventTarget(eventTarget);
176
177        Debug.Log("logEvent called: " + eventType + " (" + GetEventTargetId(eventTarget) + ")");
178
179        EventData eventData = new EventData
180            { type = eventType, target = eventTarget, timestamp = DateTime.UtcNow.Ticks,
181              x = (float) Math.Round(eventTarget.transform.position.x, 2),
182              y = (float) Math.Round(eventTarget.transform.position.y, 2),
183              z = (float) Math.Round(eventTarget.transform.position.z, 2),
184              parameters = parameters};
185
186        readWriteLockCollectedEvents.EnterUpgradeableReadLock();
187        List<EventData> eventList;
188
189        if (!collectedEvents.ContainsKey(serverURL))
190        {
191            eventList = new List<EventData>();
192            readWriteLockCollectedEvents.EnterWriteLock();
193            collectedEvents.Add(serverURL, eventList);
194            readWriteLockCollectedEvents.ExitWriteLock();
195        }
196        else
197        {
198            eventList = collectedEvents[serverURL];
199        }
200
201        eventList.Add(eventData);
202
203        if (eventList.Count >= 10)
204        {
205            SendEventsToServer(false);
206        }
207
208        readWriteLockCollectedEvents.ExitUpgradeableReadLock();
209    }
210
211    /**
212     * collects an event and triggers sending events to the server, if sufficient events have been collected.
213     * The event target considered for the given event is the game object of which the instance of this class
214     * is a component. Hence, this method must be called with care to ensure, that the correct correct
215     * event target is considered.
216     */
217    public void LogEvent(string eventType, params KeyValuePair<String, String>[] parameters)
218    {
219        LogEvent(eventType, this.gameObject, parameters);
220    }
221
222    /**
223     * used for storing event targets and their hierarchies and for calculating their unique ids
224     */
225    private void RegisterEventTarget(GameObject eventTarget)
226    {
227        // check, if the target is already registered. If so, do nothing
228        readWriteLockEventTargetIds.EnterReadLock();
229
230        if (eventTargetIds.ContainsKey(eventTarget))
231        {
232            readWriteLockEventTargetIds.ExitReadLock();
233            return;
234        }
235
236        readWriteLockEventTargetIds.ExitReadLock();
237
238        // before adding the target itself, check for the parents
239        GameObject parent = eventTarget;
240        string id = "";
241
242        while (parent != null)
243        {
244            id += parent.name;
245            if (parent.transform.parent != null)
246            {
247                parent = parent.transform.parent.gameObject;
248                RegisterEventTarget(parent);
249            }
250            else
251            {
252                parent = null;
253            }
254        }
255
256        // now, register the target. Ensure, that no other thread added it in the meantime
257        readWriteLockEventTargetIds.EnterUpgradeableReadLock();
258
259        if (eventTargetIds.ContainsKey(eventTarget))
260        {
261            readWriteLockEventTargetIds.ExitUpgradeableReadLock();
262            return;
263        }
264
265        id += eventTarget.scene.name;
266
267        id = toMD5Hash(id);
268
269        readWriteLockEventTargetIds.EnterWriteLock();
270        eventTargetIds.Add(eventTarget, id);
271        readWriteLockEventTargetIds.ExitWriteLock();
272
273        Debug.Log("registerEventTarget called: " + eventTarget.name + " (" + id + ")");
274
275        readWriteLockEventTargetIds.ExitUpgradeableReadLock();
276    }
277
278    /**
279     * returns the id calculated for an event target for its identification
280     */
281    private string GetEventTargetId(GameObject eventTarget)
282    {
283        readWriteLockEventTargetIds.EnterReadLock();
284        String result = eventTargetIds[eventTarget];
285        readWriteLockEventTargetIds.ExitReadLock();
286
287        return result;
288    }
289
290    /**
291     * sends all events to the server
292     */
293    private void SendEventsToServer(Boolean wait)
294    {
295        EventData[] eventsToSend = null;
296
297        // get a copy and directly release the lock so that further events can be collected in parallel.
298        readWriteLockCollectedEvents.EnterReadLock();
299
300        if (collectedEvents.ContainsKey(serverURL)) {
301            eventsToSend = collectedEvents[serverURL].ToArray();
302            collectedEvents[serverURL].Clear();
303        }
304
305        readWriteLockCollectedEvents.ExitReadLock();
306
307        // send events only, if there are some
308        if ((eventsToSend == null) || (eventsToSend.Length <= 0))
309        {
310            return;
311        }
312
313        Debug.Log("sending " + eventsToSend.Length + " events to the server");
314
315        String message = CreateMessage(eventsToSend);
316
317        WWW www = new WWW(serverURL, Encoding.UTF8.GetBytes(message));
318
319        if (wait)
320        {
321            while (!www.isDone)
322            {
323                new WaitForSeconds(0.1f);
324            }
325        }
326    }
327
328    /**
329     * creates a message containing the events and the corresponding targets
330     */
331    private string CreateMessage(EventData[] eventsToSend)
332    {
333        // create the message
334        StringBuilder message = new StringBuilder();
335        message.AppendLine("{");
336        message.AppendLine("  \"message\": {");
337        message.AppendLine("    \"clientInfos\": {");
338        message.AppendLine("      \"appId\": \"" + appId + "\"");
339        message.AppendLine("      \"clientId\": \"" + clientId + "\"");
340        message.AppendLine("    },");
341
342        // determine the targets to be send to the server
343        Dictionary<GameObject, List<GameObject>> targetsToSend = new Dictionary<GameObject, List<GameObject>>();
344        List<GameObject> rootTargets = new List<GameObject>();
345
346        for (int i = 0; i < eventsToSend.Length; i++)
347        {
348            GameObject targetToSend = eventsToSend[i].target;
349            GameObject lastChild = null;
350
351            while (targetToSend != null)
352            {
353                List<GameObject> children = null;
354
355                if (!targetsToSend.ContainsKey(targetToSend))
356                {
357                    children = new List<GameObject>();
358                    if (lastChild != null)
359                    {
360                        children.Add(lastChild);
361                    }
362
363                    targetsToSend.Add(targetToSend, children);
364
365                    if (targetToSend.transform.parent != null)
366                    {
367                        lastChild = targetToSend;
368                        targetToSend = targetToSend.transform.parent.gameObject;
369                    }
370                    else
371                    {
372                        rootTargets.Add(targetToSend);
373                        targetToSend = null;
374                    }
375                }
376                else
377                {
378                    if (lastChild != null)
379                    {
380                        children = targetsToSend[targetToSend];
381                        children.Add(lastChild);
382                    }
383                    targetToSend = null;
384                }
385            }
386        }
387
388        // add the targets to be send to the server
389        message.AppendLine("    \"targetStructure\": [");
390        message.AppendLine("      {");
391        message.AppendLine("        \"targetId\": \"" + toMD5Hash(SceneManager.GetActiveScene().name) + "\",");
392        message.AppendLine("        \"name\": \"" + SceneManager.GetActiveScene().name + "\",");
393        message.AppendLine("        \"children\": [");
394
395        foreach (GameObject root in rootTargets)
396        {
397            DumpTarget(message, root, targetsToSend, "        ");
398            message.AppendLine("        ,");
399        }
400
401        message.AppendLine("        ]");
402        message.AppendLine("      }");
403        message.AppendLine("    ],");
404
405        // now add the events
406        message.AppendLine("    \"events\": [");
407
408        for (int i = 0; i < eventsToSend.Length; i++)
409        {
410            message.AppendLine("      {");
411            message.AppendLine("        \"time\": \"" + eventsToSend[i].timestamp + "\",");
412            message.AppendLine("        \"type\": \"" + eventsToSend[i].type + "\",");
413            message.AppendLine("        \"targetId\": \"" + GetEventTargetId(eventsToSend[i].target) + "\",");
414            message.AppendLine("        \"targetPosition\": \"(" + eventsToSend[i].x + ", " + eventsToSend[i].y +
415                               ", " + eventsToSend[i].z + ")\",");
416            if ((eventsToSend[i].parameters != null) && (eventsToSend[i].parameters.Length > 0))
417            {
418                foreach (KeyValuePair<string, string> parameter in eventsToSend[i].parameters)
419                {
420                    message.AppendLine("        \"" + parameter.Key + "\": \"" + parameter.Value + "\",");
421                }
422            }
423
424            message.AppendLine("      },");
425        }
426
427        message.AppendLine("    ]");
428
429        message.AppendLine("  }");
430        message.AppendLine("}");
431
432        String result = message.ToString();
433        Debug.Log(result);
434
435        return result;
436    }
437
438    /**
439     * dumps a target to the given string builder by considering the indentation. Calls itself recursively to dump the target hierarchy.
440     */
441    private void DumpTarget(StringBuilder message, GameObject eventTarget, Dictionary<GameObject, List<GameObject>> targetsToSend, string indent)
442    {
443        message.Append(indent);
444        message.AppendLine("{");
445        message.Append(indent);
446        message.AppendLine("  \"targetId\": \"" + GetEventTargetId(eventTarget) + "\",");
447        message.Append(indent);
448        message.AppendLine("  \"name\": \"" + eventTarget.name + "\",");
449
450        List<GameObject> children = targetsToSend[eventTarget];
451
452        if (children.Count > 0)
453        {
454            message.Append(indent);
455            message.AppendLine("  \"children\": [");
456
457            foreach(GameObject child in children)
458            {
459                DumpTarget(message, child, targetsToSend, indent + "    ");
460                message.Append(indent);
461                message.AppendLine("    ,");
462            }
463
464            message.Append(indent);
465            message.AppendLine("  ]");
466        }
467
468        message.Append(indent);
469        message.AppendLine("}");
470    }
471
472    /**
473     * convenience method to calculate MD5 hashes for a given string
474     */
475    private static string toMD5Hash(string str)
476    {
477        MD5 md5Algorithm = new MD5CryptoServiceProvider();
478        byte[] strAsBytes = Encoding.UTF8.GetBytes(str);
479        strAsBytes = md5Algorithm.ComputeHash(strAsBytes);
480
481        return BitConverter.ToString(strAsBytes).Replace("-", "");
482    }
483
484
485    /**
486     * In this coroutine, we continuously check all game objects in the scene for changes so that we can register
487     * event listeners for objects on which they should exist. To not cause too much effort for that, the coroutine yields from
488     * time to time. The event listeners are created only once and then stored in a dictionary. This is required as event listeners
489     * can only be removed using their reference. But we need to be able to remove them to ensure that they are registered only
490     * once per object.
491     * After some time, the objects on which the event listeners are registers may be destroyed. Hence, the dictionary may
492     * contain event listeners for objects that do not exist anymore. To handle this, the coroutine also stores the last time
493     * since when the event listeners have been considered last during a registration cycle of this coroutine. If this point
494     * in time is more than 5 seconds ago, the coroutine considers the object to be destroyed and, therefore, also destroys
495     * the event listeners.
496     */
497    private IEnumerator EventHandlingRegistrationCoroutine()
498    {
499        List<GameObject> currentRootGameObjects = new List<GameObject>(100);
500        int rootGameObjectsPointer = 0;
501        LinkedList<UnityEngine.Object> currentRelevantGameObjects = new LinkedList<UnityEngine.Object>();
502        int gameObjectsHandledPerFrame = 10;
503
504        while (true)
505        {
506            if (rootGameObjectsPointer > (currentRootGameObjects.Count - 1))
507            {
508                // we need to reread the root objects of all loaded scenes
509                for (int i = 0; i < SceneManager.sceneCount; i++)
510                {
511                    Scene scene = SceneManager.GetSceneAt(i);
512                    if (scene.isLoaded)
513                    {
514                        currentRootGameObjects.AddRange(scene.GetRootGameObjects());
515                    }
516                }
517               
518                rootGameObjectsPointer = 0;
519
520                // yield and continue in next frame
521                yield return null;
522                //print("considering " + currentRootGameObjects.Count + " root objects");
523            }
524
525            // check, if there are enough game objects to be processed in the current frame
526            while ((currentRelevantGameObjects.Count < gameObjectsHandledPerFrame) &&
527                   (rootGameObjectsPointer < currentRootGameObjects.Count))
528            {
529                // we need to read some further game objects into the list of those to be handled next
530                addRelevantGameObjects(currentRootGameObjects[rootGameObjectsPointer++], currentRelevantGameObjects);
531                yield return null;
532            }
533
534            //print("considering next " + currentRelevantGameObjects.Count + " game objects for which actions must be logged");
535
536            // now handle the relevant objects, but at most gameObjectsHandledPerFrame at once
537            readWriteLockCollectedEvents.EnterWriteLock();
538            int currentlyHandled = 0;
539            while (currentRelevantGameObjects.Count > 0)
540            {
541                UnityEngine.Object relevantObject = currentRelevantGameObjects.First.Value;
542
543                currentRelevantGameObjects.RemoveFirst();
544                //print("ensuring event handling for " + relevantObject);
545                ensureEventHandling(relevantObject);
546                currentlyHandled++;
547
548                if (currentlyHandled % gameObjectsHandledPerFrame == 0)
549                {
550                    // we handled enough relevant game objects in this frame. Release the lock, yield, and continue in next frame
551                    // by obtaining the lock again
552                    readWriteLockCollectedEvents.ExitWriteLock();
553                    yield return null;
554                    readWriteLockCollectedEvents.EnterWriteLock();
555                }
556            }
557            readWriteLockCollectedEvents.ExitWriteLock();
558
559            yield return null;
560
561            readWriteLockCollectedEvents.EnterWriteLock();
562            // now check for any listener in the dictionary, if it was touched in the last cycles, and if not, destroy them
563            long thresholdTime = DateTime.UtcNow.Ticks - 50000000;
564            List<UnityEngine.Object> obsoleteEntries = new List<UnityEngine.Object>();
565            foreach (KeyValuePair<UnityEngine.Object, Listeners> entry in registeredUnityActions)
566            {
567                if (entry.Value.lastTouched < thresholdTime)
568                {
569                    obsoleteEntries.Add(entry.Key);
570                }
571            }
572
573            foreach (UnityEngine.Object obsoleteEntry in obsoleteEntries)
574            {
575                registeredUnityActions.Remove(obsoleteEntry);
576            }
577            readWriteLockCollectedEvents.ExitWriteLock();
578
579            if (rootGameObjectsPointer > (currentRootGameObjects.Count - 1))
580            {
581                // we now checked all relevant objects. Let's wait a second before starting the next registration cycle
582                yield return new WaitForSeconds(1f);
583                //yield return null;
584            }
585            else
586            {
587                yield return null;
588            }
589        }
590    }
591
592    /**
593     * Traverses a hierarchy of game objects and adds game objects relevant for event logging to the given list
594     */
595    virtual protected void addRelevantGameObjects(GameObject gameObject, LinkedList<UnityEngine.Object> currentRelevantGameObjects)
596    {
597        if (gameObject)
598        {
599            Component[] selectables = gameObject.GetComponentsInChildren(typeof(Selectable));
600            Component[] scrollRects = gameObject.GetComponentsInChildren(typeof(ScrollRect));
601            Component[] eventTriggers = gameObject.GetComponentsInChildren(typeof(EventTrigger));
602
603            foreach (Component relevantObject in selectables)
604            {
605                currentRelevantGameObjects.AddLast(relevantObject);
606            }
607
608            foreach (Component relevantObject in scrollRects)
609            {
610                currentRelevantGameObjects.AddLast(relevantObject);
611            }
612
613            foreach (Component relevantObject in eventTriggers)
614            {
615                currentRelevantGameObjects.AddLast(relevantObject);
616            }
617        }
618    }
619
620    /**
621     * registers event handling for the given relevant object
622     */
623    virtual protected void ensureEventHandling(UnityEngine.Object relevantObject)
624    {
625        if (relevantObject is Button)
626        {
627            ensureEventHandling(((Button)relevantObject).onClick, "PointerClick", ((Button)relevantObject).gameObject);
628        }
629        else if (relevantObject is Toggle)
630        {
631            ensureEventHandling(((Toggle)relevantObject).onValueChanged, "ValueChanged", ((Toggle)relevantObject).gameObject);
632        }
633        else if (relevantObject is Slider)
634        {
635            ensureEventHandling(((Slider)relevantObject).onValueChanged, "ValueChanged", ((Slider)relevantObject).gameObject);
636        }
637        else if (relevantObject is Scrollbar)
638        {
639            ensureEventHandling(((Scrollbar)relevantObject).onValueChanged, "ValueChanged", ((Scrollbar)relevantObject).gameObject);
640        }
641        else if (relevantObject is Dropdown)
642        {
643            ensureEventHandling(((Dropdown)relevantObject).onValueChanged, "ValueChanged", ((Dropdown)relevantObject).gameObject);
644        }
645        else if (relevantObject is InputField)
646        {
647            ensureEventHandling(((InputField)relevantObject).onValueChanged, "ValueChanged", ((InputField)relevantObject).gameObject);
648            ensureEventHandling(((InputField)relevantObject).onEndEdit, "EditingEnded", ((InputField)relevantObject).gameObject);
649        }
650        else if (relevantObject is ScrollRect)
651        {
652            ensureEventHandling(((ScrollRect)relevantObject).onValueChanged, "ValueChanged", ((ScrollRect)relevantObject).gameObject);
653        }
654        else if (relevantObject is EventTrigger)
655        {
656            foreach (EventTrigger.Entry entry in ((EventTrigger)relevantObject).triggers)
657            {
658                ensureEventHandling(entry.callback, entry.eventID.ToString(), ((EventTrigger)relevantObject).gameObject);
659            }
660        }
661        else
662        {
663            Debug.Log("unknown type of relevant object: " + relevantObject.GetType());
664        }
665    }
666
667    /**
668     * ensures that there is a listener for the given event on the given game object
669     */
670    private void ensureEventHandling(UnityEvent unityEvent, string eventType, GameObject eventTarget)
671    {
672        UnityAction listener = getListeners(eventTarget).getCreateListener(eventType, eventTarget);
673
674        // remove the listener first to ensure, it is not registered twice
675        unityEvent.RemoveListener(listener);
676        unityEvent.AddListener(listener);
677    }
678
679    /**
680     * ensures that there is a listener for the given event on the given game object
681     */
682    private void ensureEventHandling<T>(UnityEvent<T> unityEvent, string eventType, GameObject eventTarget)
683    {
684        UnityAction<T> listener = getListeners(eventTarget).getCreateListener<T>(eventType, eventTarget);
685
686        // remove the listener first to ensure, it is not registered twice
687        unityEvent.RemoveListener(listener);
688        unityEvent.AddListener(listener);
689    }
690
691    /**
692     * Determines the event listeners for a given eventTarget
693     */
694    protected Listeners getListeners(GameObject eventTarget)
695    {
696        Listeners listeners = null;
697
698        registeredUnityActions.TryGetValue(eventTarget, out listeners);
699
700        if (listeners == null)
701        {
702            listeners = createListeners();
703            registeredUnityActions.Add(eventTarget, listeners);
704        }
705
706        return listeners;
707    }
708
709    /**
710     * used to allow subclasses to create other, more concrete types of listeners
711     */
712    virtual protected Listeners createListeners()
713    {
714        return new Listeners(LogEvent);
715    }
716
717    /**
718     * In this coroutine, we continuously check the position of the main camera to do movement tracking.
719     * For this the coroutine runs every 100 ms and compares the preceding location with the current one.
720     * If their is a change, then a motion is considered. If there is no change, then no motion is
721     * considered. If before there was a motion, then an event is logged. If the motion continues but
722     * the direction vectors of the previous and the current motion are more different than 5°, also
723     * an event is logged for the first motion.
724     */
725    private IEnumerator MovementTrackingCoroutine()
726    {
727        Vector3 previousPosition = Camera.main.transform.position;
728        Vector3 currentTranslationVector = new Vector3(0, 0, 0);
729        bool headIsMoving = false;
730        long timestamp = 0;
731
732        while (true)
733        {
734            Vector3 currentPosition = Camera.main.transform.position;
735            Vector3 translationVector = currentPosition - previousPosition;
736
737            if (translationVector.magnitude > 0.05)
738            {
739                // movement detected (position change larger than 5cm). Check if we are already in a movement
740                if (headIsMoving)
741                {
742                    // the movement is continuing. Check, if it is almost the same direction
743                    if (Vector3.Angle(currentTranslationVector, translationVector) < 5)
744                    {
745                        // we are still almost in the same direction. Extend the current translation vector
746                        // and continue
747                        currentTranslationVector += translationVector;
748                    }
749                    else
750                    {
751                        // the movement changed the direction. Therefore, log an event for the previous movement
752                        // and set everything as if the new movement just started
753                        Vector3 source = previousPosition - currentTranslationVector;
754                        roundVector(ref source);
755                        Vector3 dest = previousPosition;
756                        roundVector(ref dest);
757
758                        LogEvent("headMoved", Camera.main.gameObject,
759                                 new KeyValuePair<string, string>("movementStart", timestamp.ToString()),
760                                 new KeyValuePair<string, string>("movementStop", DateTime.UtcNow.Ticks.ToString()),
761                                 new KeyValuePair<string, string>("from", "(" + source.x + ", " + source.y +
762                                                                  ", " + source.z + ")"),
763                                 new KeyValuePair<string, string>("to", "(" + dest.x + ", " + dest.y +
764                                                                  ", " + dest.z + ")"));
765
766                        currentTranslationVector = translationVector;
767                        timestamp = DateTime.UtcNow.Ticks;
768                    }
769                }
770                else
771                {
772                    // head is started moving--> store the translation vector, set the flag, and continue
773                    currentTranslationVector = translationVector;
774                    headIsMoving = true;
775                    timestamp = DateTime.UtcNow.Ticks;
776                }
777            }
778            else
779            {
780                if (headIsMoving)
781                {
782                    // the movement of the head stopped --> log event and store that movement stopped
783                    Vector3 source = previousPosition - currentTranslationVector;
784                    roundVector(ref source);
785                    Vector3 dest = previousPosition;
786                    roundVector(ref dest);
787
788                    LogEvent("headMoved", Camera.main.gameObject,
789                             new KeyValuePair<string, string>("movementStart", timestamp.ToString()),
790                             new KeyValuePair<string, string>("movementStop", DateTime.UtcNow.Ticks.ToString()),
791                             new KeyValuePair<string, string>("from", "(" + source.x + ", " + source.y +
792                                                              ", " + source.z + ")"),
793                             new KeyValuePair<string, string>("to", "(" + dest.x + ", " + dest.y +
794                                                              ", " + dest.z + ")"));
795
796                    currentTranslationVector = new Vector3(0, 0, 0);
797                    headIsMoving = false;
798                    timestamp = 0;
799                }
800                // else no movement. Do nothing.
801            }
802
803            previousPosition = currentPosition;
804
805            // wait for the next execution
806            yield return new WaitForSeconds(0.1f);
807        }
808    }
809
810    /**
811     * In this coroutine, we continuously check the rotation of the main camera to do rotation tracking.
812     * For this the coroutine runs every 100 ms and compares the preceding rotation with the current one.
813     * If their is a change, then a rotation is considered. If there is no change, then no rotation is
814     * considered. If before there was a rotation, then an event is logged.
815     */
816    private IEnumerator GazeTrackingCoroutine()
817    {
818        Vector3 initialOrientation = new Vector3(0, 0, 0);
819        Vector3 previousOrientation = Camera.main.transform.forward;
820        float currentRotationAngle = 0f;
821        bool headIsRotating = false;
822        long timestamp = 0;
823
824        while (true)
825        {
826            Vector3 currentOrientation = Camera.main.transform.forward;
827            float rotationAngle = Vector3.Angle(previousOrientation, currentOrientation);
828
829            if (rotationAngle > 2)
830            {
831                // rotation detected (gaze direction change larger than 5°). Check if we are already in a rotation
832                if (headIsRotating)
833                {
834                    // the rotation is continuing. Extend the current rotation angle and continue
835                    currentRotationAngle += rotationAngle;
836                }
837                else
838                {
839                    // head has started rotating--> store the roation angle, set the flag, and continue
840                    initialOrientation = previousOrientation;
841                    currentRotationAngle = rotationAngle;
842                    headIsRotating = true;
843                    timestamp = DateTime.UtcNow.Ticks;
844                }
845            }
846            else
847            {
848                if (headIsRotating)
849                {
850                    // the rotation of the head stopped --> log event and store that rotation stopped
851                    Vector3 source = initialOrientation;
852                    roundVector(ref source);
853                    Vector3 dest = currentOrientation;
854                    roundVector(ref dest);
855
856                    LogEvent("headRotated", Camera.main.gameObject,
857                             new KeyValuePair<string, string>("rotationStart", timestamp.ToString()),
858                             new KeyValuePair<string, string>("rotationStop", DateTime.UtcNow.Ticks.ToString()),
859                             new KeyValuePair<string, string>("from", "(" + source.x + ", " + source.y +
860                                                              ", " + source.z + ")"),
861                             new KeyValuePair<string, string>("to", "(" + dest.x + ", " + dest.y +
862                                                              ", " + dest.z + ")"));
863
864                    initialOrientation = new Vector3(0, 0, 0);
865                    currentRotationAngle = 0;
866                    headIsRotating = false;
867                    timestamp = 0;
868                }
869                // else no movement. Do nothing.
870            }
871
872            previousOrientation = currentOrientation;
873
874            // wait for the next execution
875            yield return new WaitForSeconds(0.1f);
876        }
877    }
878
879    /**
880     * convenience method to round the values of a vector to two decimal positions
881     */
882    private void roundVector(ref Vector3 vector)
883    {
884        vector.x = (float) Math.Round(vector.x, 2);
885        vector.y = (float) Math.Round(vector.y, 2);
886        vector.z = (float) Math.Round(vector.z, 2);
887    }
888
889    /**
890     * inner structure to use for storing event data
891     */
892    private struct EventData
893    {
894        public string type;
895        public GameObject target;
896        public long timestamp;
897        public float x;
898        public float y;
899        public float z;
900        public KeyValuePair<string, string>[] parameters;
901    }
902
903    /**
904     * inner structure to use for storing listeners for scene objects
905     */
906    protected class Listeners
907    {
908        /** the delegate for callback methods to call when events occur */
909        public delegate void EventHandlingCallback(string eventType, GameObject eventTarget, params KeyValuePair<String, String>[] parameters);
910
911        /** stores when this information has been touched the last time */
912        internal long lastTouched = DateTime.UtcNow.Ticks;
913
914        /** stores the list of listener managed in this structure */
915        protected List<KeyValuePair<string, object>> listeners = new List<KeyValuePair<string, object>>();
916
917        /** the callback method to call if an event occurs */
918        protected EventHandlingCallback callback;
919
920        /**
921         * used to pass the event handling callback method
922         */
923        public Listeners(EventHandlingCallback callback)
924        {
925            this.callback = callback;
926        }
927
928        /**
929         * returns a listener for the given event type and updates the internal timestamp. If there is no listener
930         * one will be created and registered.
931         */
932        internal UnityAction getCreateListener(string eventType, GameObject eventTarget)
933        {
934            UnityAction listener = null;
935
936            foreach (KeyValuePair<string, object> candidate in listeners)
937            {
938                if (candidate.Key == eventType)
939                {
940                    listener = (UnityAction)candidate.Value;
941                    break;
942                }
943            }
944
945            if (listener == null)
946            {
947                listener = delegate { callback(eventType, eventTarget); };
948                addListener(eventType, listener);
949                //Debug.Log("ensuring handling of event " + eventType + " for  " + eventTarget);
950            }
951
952            lastTouched = DateTime.UtcNow.Ticks;
953            return listener;
954        }
955
956        /**
957         * returns a listener for the given event type and updates the internal timestamp. If there is no listener
958         * one will be created and registered.
959         */
960        internal UnityAction<T> getCreateListener<T>(string eventType, GameObject eventTarget)
961        {
962            UnityAction<T> listener = null;
963
964            foreach (KeyValuePair<string, object> candidate in listeners)
965            {
966                if (candidate.Key == eventType)
967                {
968                    listener = (UnityAction<T>)candidate.Value;
969                    break;
970                }
971            }
972
973            if (listener == null)
974            {
975                listener = delegate (T arg)
976                {
977                    if (arg != null)
978                    {
979                        if (arg.Equals(eventTarget))
980                        {
981                            callback(eventType, eventTarget);
982                        }
983                        else if (arg is PointerEventData)
984                        {
985                            PointerEventData eventData = arg as PointerEventData;
986                            callback(eventType, eventTarget,
987                                     new KeyValuePair<string, string>("distance", eventData.pointerCurrentRaycast.distance.ToString()));
988                        }
989                        else
990                        {
991                            callback(eventType, eventTarget, new KeyValuePair<string, string>("value", arg.ToString()));
992                        }
993                    }
994                    else
995                    {
996                        callback(eventType, eventTarget);
997                    }
998                };
999
1000                addListener(eventType, listener);
1001                //Debug.Log("ensuring handling of event " + eventType + " for  " + eventTarget);
1002            }
1003
1004            lastTouched = DateTime.UtcNow.Ticks;
1005            return listener;
1006        }
1007
1008        /**
1009         * Adds a listener to the internal listener list while also updating the timestamp
1010         */
1011        internal void addListener(string eventType, object listener)
1012        {
1013            listeners.Add(new KeyValuePair<string, object>(eventType, listener));
1014            lastTouched = DateTime.UtcNow.Ticks;
1015        }
1016    }
1017
1018}
1019