ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
class.ilLMTracker.php
Go to the documentation of this file.
1 <?php
2 
3 /* Copyright (c) 1998-2014 ILIAS open source, Extended GPL, see docs/LICENSE */
4 
13 {
17  protected $db;
18 
22  protected $lng;
23 
27  protected $plugin_admin;
28 
32  protected $user;
33 
34  const NOT_ATTEMPTED = 0;
35  const IN_PROGRESS = 1;
36  const COMPLETED = 2;
37  const FAILED = 3;
38  const CURRENT = 99;
39 
40  protected $lm_ref_id;
41  protected $lm_obj_id;
42  protected $lm_tree;
43  protected $lm_obj_ids = array();
44  protected $tree_arr = array(); // tree array
45  protected $re_arr = array(); // read event data array
46  protected $loaded_for_node = false; // current node for that the tracking data has been loaded
47  protected $dirty = false;
48  protected $page_questions = array();
49  protected $all_questions = array();
50  protected $answer_status = array();
51  protected $has_incorrect_answers = false;
52  protected $current_page_id = 0;
53 
54  public static $instances = array();
55  public static $instancesbyobj = array();
56 
60 
66  private function __construct($a_id, $a_by_obj_id = false, $a_user_id)
67  {
68  global $DIC;
69 
70  $this->db = $DIC->database();
71  $this->lng = $DIC->language();
72  $this->plugin_admin = $DIC["ilPluginAdmin"];
73  $this->user = $DIC->user();
74  $this->user_id = $a_user_id;
75 
76  if ($a_by_obj_id) {
77  $this->lm_ref_id = 0;
78  $this->lm_obj_id = $a_id;
79  } else {
80  $this->lm_ref_id = $a_id;
81  $this->lm_obj_id = ilObject::_lookupObjId($a_id);
82  }
83 
84  include_once("./Modules/LearningModule/classes/class.ilLMTree.php");
85  $this->lm_tree = ilLMTree::getInstance($this->lm_obj_id);
86  }
87 
94  public static function getInstance($a_ref_id, $a_user_id = 0)
95  {
96  global $DIC;
97 
98  $ilUser = $DIC->user();
99 
100  if ($a_user_id == 0) {
101  $a_user_id = $ilUser->getId();
102  }
103 
104  if (!isset(self::$instances[$a_ref_id][$a_user_id])) {
105  self::$instances[$a_ref_id][$a_user_id] = new ilLMTracker($a_ref_id, false, $a_user_id);
106  }
107  return self::$instances[$a_ref_id][$a_user_id];
108  }
109 
116  public static function getInstanceByObjId($a_obj_id, $a_user_id = 0)
117  {
118  global $DIC;
119 
120  $ilUser = $DIC->user();
121 
122  if ($a_user_id == 0) {
123  $a_user_id = $ilUser->getId();
124  }
125 
126  if (!isset(self::$instancesbyobj[$a_obj_id][$a_user_id])) {
127  self::$instancesbyobj[$a_obj_id][$a_user_id] = new ilLMTracker($a_obj_id, true, $a_user_id);
128  }
129  return self::$instancesbyobj[$a_obj_id][$a_user_id];
130  }
131 
135 
141  public function trackAccess($a_page_id, $user_id)
142  {
143  if ($user_id == ANONYMOUS_USER_ID) {
144  ilChangeEvent::_recordReadEvent("lm", $this->lm_ref_id, $this->lm_obj_id, $user_id);
145  return;
146  }
147 
148  if ($this->lm_ref_id == 0) {
149  throw new ilLMPresentationException("ilLMTracker: No Ref Id given.");
150  }
151 
152  // track page and chapter access
153  $this->trackPageAndChapterAccess($a_page_id);
154 
155  // track last page access (must be done after calling trackPageAndChapterAccess())
156  $this->trackLastPageAccess($this->user_id, $this->lm_ref_id, $a_page_id);
157 
158  // #9483
159  // general learning module lp tracking
160  include_once("./Services/Tracking/classes/class.ilLearningProgress.php");
162  $this->user_id,
163  $this->lm_obj_id,
164  $this->lm_ref_id,
165  "lm"
166  );
167 
168  // obsolete?
169  include_once("./Services/Tracking/classes/class.ilLPStatusWrapper.php");
170  ilLPStatusWrapper::_updateStatus($this->lm_obj_id, $this->user_id);
171 
172  // mark currently loaded data as dirty to force reload if necessary
173  $this->dirty = true;
174  }
175 
183  public function trackLastPageAccess($usr_id, $lm_id, $obj_id)
184  {
185  $title = "";
186  $db = $this->db;
187  $db->replace(
188  "lo_access",
189  [
190  "usr_id" => ["integer", $usr_id],
191  "lm_id" => ["integer", $lm_id]
192  ],
193  [
194  "timestamp" => ["timestamp", ilUtil::now()],
195  "obj_id" => ["integer", $obj_id],
196  "lm_title" => ["text", $title]
197  ]
198  );
199  }
200 
201 
205  protected function trackPageAndChapterAccess($a_page_id)
206  {
207  $ilDB = $this->db;
208 
209  $now = time();
210 
211  //
212  // 1. Page access: current page
213  //
214  $set = $ilDB->query("SELECT obj_id FROM lm_read_event" .
215  " WHERE obj_id = " . $ilDB->quote($a_page_id, "integer") .
216  " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
217  if (!$ilDB->fetchAssoc($set)) {
218  $fields = array(
219  "obj_id" => array("integer", $a_page_id),
220  "usr_id" => array("integer", $this->user_id)
221  );
222  // $ilDB->insert("lm_read_event", $fields);
223  $ilDB->replace("lm_read_event", $fields, array()); // #15144
224  }
225 
226  // update all parent chapters
227  $ilDB->manipulate("UPDATE lm_read_event SET" .
228  " read_count = read_count + 1 " .
229  " , last_access = " . $ilDB->quote($now, "integer") .
230  " WHERE obj_id = " . $ilDB->quote($a_page_id, "integer") .
231  " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
232 
233 
234  //
235  // 2. Chapter access: based on last page accessed
236  //
237 
238  // get last accessed page
239  $set = $ilDB->query("SELECT * FROM lo_access WHERE " .
240  "usr_id = " . $ilDB->quote($this->user_id, "integer") . " AND " .
241  "lm_id = " . $ilDB->quote($this->lm_ref_id, "integer"));
242  $res = $ilDB->fetchAssoc($set);
243  if ($res["obj_id"]) {
244  include_once('Services/Tracking/classes/class.ilObjUserTracking.php');
245  $valid_timespan = ilObjUserTracking::_getValidTimeSpan();
246 
247  $pg_ts = new ilDateTime($res["timestamp"], IL_CAL_DATETIME);
248  $pg_ts = $pg_ts->get(IL_CAL_UNIX);
249  $pg_id = $res["obj_id"];
250  if (!$this->lm_tree->isInTree($pg_id)) {
251  return;
252  }
253 
254  $time_diff = $read_diff = 0;
255 
256  // spent_seconds or read_count ?
257  if (($now - $pg_ts) <= $valid_timespan) {
258  $time_diff = $now - $pg_ts;
259  } else {
260  $read_diff = 1;
261  }
262 
263  // find parent chapter(s) for that page
264  $parent_st_ids = array();
265  foreach ($this->lm_tree->getPathFull($pg_id) as $item) {
266  if ($item["type"] == "st") {
267  $parent_st_ids[] = $item["obj_id"];
268  }
269  }
270 
271  if ($parent_st_ids && ($time_diff || $read_diff)) {
272  // get existing chapter entries
273  $ex_st = array();
274  $set = $ilDB->query("SELECT obj_id FROM lm_read_event" .
275  " WHERE " . $ilDB->in("obj_id", $parent_st_ids, "", "integer") .
276  " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
277  while ($row = $ilDB->fetchAssoc($set)) {
278  $ex_st[] = $row["obj_id"];
279  }
280 
281  // add missing chapter entries
282  $missing_st = array_diff($parent_st_ids, $ex_st);
283  if (sizeof($missing_st)) {
284  foreach ($missing_st as $st_id) {
285  $fields = array(
286  "obj_id" => array("integer", $st_id),
287  "usr_id" => array("integer", $this->user_id)
288  );
289  // $ilDB->insert("lm_read_event", $fields);
290  $ilDB->replace("lm_read_event", $fields, array()); // #15144
291  }
292  }
293 
294  // update all parent chapters
295  $ilDB->manipulate("UPDATE lm_read_event SET" .
296  " read_count = read_count + " . $ilDB->quote($read_diff, "integer") .
297  " , spent_seconds = spent_seconds + " . $ilDB->quote($time_diff, "integer") .
298  " , last_access = " . $ilDB->quote($now, "integer") .
299  " WHERE " . $ilDB->in("obj_id", $parent_st_ids, "", "integer") .
300  " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
301  }
302  }
303  }
304 
305 
309 
315  public function setCurrentPage($a_val)
316  {
317  $this->current_page_id = $a_val;
318  }
319 
325  public function getCurrentPage()
326  {
327  return $this->current_page_id;
328  }
329 
336  protected function loadLMTrackingData()
337  {
338  $ilDB = $this->db;
339 
340  // we must prevent loading tracking data multiple times during a request where possible
341  // please note that the dirty flag works only to a certain limit
342  // e.g. if questions are answered the flag is not set (yet)
343  // or if pages/chapter are added/deleted the flag is not set
344  if ($this->loaded_for_node === (int) $this->getCurrentPage() && !$this->dirty) {
345  return;
346  }
347 
348  $this->loaded_for_node = (int) $this->getCurrentPage();
349  $this->dirty = false;
350 
351  // load lm tree in array
352  $this->tree_arr = array();
353  $nodes = $this->lm_tree->getCompleteTree();
354  foreach ($nodes as $node) {
355  $this->tree_arr["childs"][$node["parent"]][] = $node;
356  $this->tree_arr["parent"][$node["child"]] = $node["parent"];
357  $this->tree_arr["nodes"][$node["child"]] = $node;
358  }
359 
360  // load all lm obj ids of learning module
361  include_once("./Modules/LearningModule/classes/class.ilLMObject.php");
362  $this->lm_obj_ids = ilLMObject::_getAllLMObjectsOfLM($this->lm_obj_id);
363 
364  // load read event data
365  $this->re_arr = array();
366  $set = $ilDB->query("SELECT * FROM lm_read_event " .
367  " WHERE " . $ilDB->in("obj_id", $this->lm_obj_ids, false, "integer") .
368  " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
369  while ($rec = $ilDB->fetchAssoc($set)) {
370  $this->re_arr[$rec["obj_id"]] = $rec;
371  }
372 
373  // load question/pages information
374  $this->page_questions = array();
375  $this->all_questions = array();
376  include_once("./Modules/LearningModule/classes/class.ilLMPageObject.php");
377  $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
378  foreach ($q["set"] as $quest) {
379  $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
380  $this->all_questions[] = $quest["question_id"];
381  }
382 
383  // load question answer information
384  include_once("./Services/COPage/classes/class.ilPageQuestionProcessor.php");
385  $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions, $this->user_id);
386 
387  $this->has_incorrect_answers = false;
388 
389  $has_pred_incorrect_answers = false;
390  $has_pred_incorrect_not_unlocked_answers = false;
391  $this->determineProgressStatus($this->lm_tree->readRootId(), $has_pred_incorrect_answers, $has_pred_incorrect_not_unlocked_answers);
392 
393  $this->has_incorrect_answers = $has_pred_incorrect_answers;
394  }
395 
401  public function getAllQuestionsCorrect()
402  {
403  $this->loadLMTrackingData();
404  if (count($this->all_questions) > 0 && !$this->has_incorrect_answers) {
405  return true;
406  }
407  return false;
408  }
409 
410 
417  protected function determineProgressStatus($a_obj_id, &$a_has_pred_incorrect_answers, &$a_has_pred_incorrect_not_unlocked_answers)
418  {
419  $status = ilLMTracker::NOT_ATTEMPTED;
420 
421  if (isset($this->tree_arr["nodes"][$a_obj_id])) {
422  $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
423  $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
424 
425  if (is_array($this->tree_arr["childs"][$a_obj_id])) {
426  // sort childs in correct order
427  $this->tree_arr["childs"][$a_obj_id] = ilUtil::sortArray($this->tree_arr["childs"][$a_obj_id], "lft", "asc", true);
428 
429  $cnt_completed = 0;
430  foreach ($this->tree_arr["childs"][$a_obj_id] as $c) {
431  // if child is not activated/displayed count child as implicitly completed
432  // rationale: everything that is visible for the learner determines the status
433  // see also bug #14642
434  if (!self::_isNodeVisible($c)) {
435  $cnt_completed++;
436  continue;
437  }
438  $c_stat = $this->determineProgressStatus(
439  $c["child"],
440  $a_has_pred_incorrect_answers,
441  $a_has_pred_incorrect_not_unlocked_answers
442  );
443  if ($status != ilLMTracker::FAILED) {
444  if ($c_stat == ilLMTracker::FAILED) {
445  $status = ilLMTracker::IN_PROGRESS;
446  } elseif ($c_stat == ilLMTracker::IN_PROGRESS) {
447  $status = ilLMTracker::IN_PROGRESS;
448  } elseif ($c_stat == ilLMTracker::COMPLETED || $c_stat == ilLMTracker::CURRENT) {
449  $status = ilLMTracker::IN_PROGRESS;
450  $cnt_completed++;
451  }
452  }
453  // if an item is failed or in progress or (not attempted and contains questions)
454  // the next item has predecessing incorrect answers
455  if ($this->tree_arr["nodes"][$c["child"]]["type"] == "pg") {
456  if ($c_stat == ilLMTracker::FAILED || $c_stat == ilLMTracker::IN_PROGRESS ||
457  ($c_stat == ilLMTracker::NOT_ATTEMPTED && is_array($this->page_questions[$c["child"]]) && count($this->page_questions[$c["child"]]) > 0)) {
458  $a_has_pred_incorrect_answers = true;
459  if (!$this->tree_arr["nodes"][$c["child"]]["unlocked"]) {
460  $a_has_pred_incorrect_not_unlocked_answers = true;
461  }
462  }
463  }
464  }
465  if ($cnt_completed == count($this->tree_arr["childs"][$a_obj_id])) {
466  $status = ilLMTracker::COMPLETED;
467  }
468  } elseif ($this->tree_arr["nodes"][$a_obj_id]["type"] == "pg") {
469  // check read event data
470  if (isset($this->re_arr[$a_obj_id]) && $this->re_arr[$a_obj_id]["read_count"] > 0) {
471  $status = ilLMTracker::COMPLETED;
472  } elseif ($a_obj_id == $this->getCurrentPage()) {
473  $status = ilLMTracker::CURRENT;
474  }
475 
476  $unlocked = false;
477  if (is_array($this->page_questions[$a_obj_id])) {
478  // check questions, if one is failed -> failed
479  $unlocked = true;
480  foreach ($this->page_questions[$a_obj_id] as $q_id) {
481  if (is_array($this->answer_status[$q_id])
482  && $this->answer_status[$q_id]["try"] > 0
483  && !$this->answer_status[$q_id]["passed"]) {
484  $status = ilLMTracker::FAILED;
485  if (!$this->answer_status[$q_id]["unlocked"]) {
486  $unlocked = false;
487  }
488  }
489  }
490 
491  // check questions, if one is not answered -> in progress
492  if ($status != ilLMTracker::FAILED) {
493  foreach ($this->page_questions[$a_obj_id] as $q_id) {
494  if (!is_array($this->answer_status[$q_id])
495  || $this->answer_status[$q_id]["try"] == 0) {
496  if ($status != ilLMTracker::NOT_ATTEMPTED) {
497  $status = ilLMTracker::IN_PROGRESS;
498  }
499  }
500  }
501  $unlocked = false;
502  }
503  }
504  $this->tree_arr["nodes"][$a_obj_id]["unlocked"] = $unlocked;
505  $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
506  $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
507  }
508  } else { // free pages (currently not called, since only walking through tree structure)
509  }
510  $this->tree_arr["nodes"][$a_obj_id]["status"] = $status;
511 
512  return $status;
513  }
514 
515 
523  public function getIconForLMObject($a_node, $a_highlighted_node = 0)
524  {
525  $this->loadLMTrackingData();
526  if ($a_node["child"] == $a_highlighted_node) {
527  return ilUtil::getImagePath('scorm/running.svg');
528  }
529  if (isset($this->tree_arr["nodes"][$a_node["child"]])) {
530  switch ($this->tree_arr["nodes"][$a_node["child"]]["status"]) {
532  return ilUtil::getImagePath('scorm/incomplete.svg');
533 
534  case ilLMTracker::FAILED:
535  return ilUtil::getImagePath('scorm/failed.svg');
536 
538  return ilUtil::getImagePath('scorm/completed.svg');
539  }
540  }
541  return ilUtil::getImagePath('scorm/not_attempted.svg');
542  }
543 
550  public function hasPredIncorrectAnswers($a_obj_id, $a_ignore_unlock = false)
551  {
552  $this->loadLMTrackingData();
553  $ret = false;
554  if (is_array($this->tree_arr["nodes"][$a_obj_id])) {
555  if ($a_ignore_unlock) {
556  $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"];
557  } else {
558  $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"];
559  }
560  }
561  return $ret;
562  }
563 
567 
574  public function getBlockedUsersInformation()
575  {
576  $ilDB = $this->db;
577  $lng = $this->lng;
578  $ilPluginAdmin = $this->plugin_admin;
580 
581  $blocked_users = array();
582 
583  // load question/pages information
584  $this->page_questions = array();
585  $this->all_questions = array();
586  $page_for_question = array();
587  include_once("./Modules/LearningModule/classes/class.ilLMPageObject.php");
588  $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
589  foreach ($q["set"] as $quest) {
590  $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
591  $this->all_questions[] = $quest["question_id"];
592  $page_for_question[$quest["question_id"]] = $quest["page_id"];
593  }
594  // get question information
595  include_once("./Modules/TestQuestionPool/classes/class.ilAssQuestionList.php");
596  $qlist = new ilAssQuestionList($ilDB, $lng, $ilPluginAdmin);
597  $qlist->setParentObjId(0);
598  $qlist->setJoinObjectData(false);
599  $qlist->addFieldFilter("question_id", $this->all_questions);
600  $qlist->load();
601  $qdata = $qlist->getQuestionDataArray();
602 
603  // load question answer information
604  include_once("./Services/COPage/classes/class.ilPageQuestionProcessor.php");
605  $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions);
606  include_once("./Modules/LearningModule/classes/class.ilLMPageObject.php");
607  foreach ($this->answer_status as $as) {
608  if ($as["try"] >= $qdata[$as["qst_id"]]["nr_of_tries"] && $qdata[$as["qst_id"]]["nr_of_tries"] > 0 && !$as["passed"]) {
609  //var_dump($qdata[$as["qst_id"]]);
610  $name = ilObjUser::_lookupName($as["user_id"]);
611  $as["user_name"] = $name["lastname"] . ", " . $name["firstname"] . " [" . $name["login"] . "]";
612  $as["question_text"] = $qdata[$as["qst_id"]]["question_text"];
613  $as["page_id"] = $page_for_question[$as["qst_id"]];
614  $as["page_title"] = ilLMPageObject::_lookupTitle($as["page_id"]);
615  $blocked_users[] = $as;
616  }
617  }
618 
619  return $blocked_users;
620  }
621 
628  public static function _isNodeVisible($a_node)
629  {
630  include_once("./Services/COPage/classes/class.ilPageObject.php");
631 
632  if ($a_node["type"] != "pg") {
633  return true;
634  }
635 
636  $lm_set = new ilSetting("lm");
637  $active = ilPageObject::_lookupActive(
638  $a_node["child"],
639  "lm",
640  $lm_set->get("time_scheduled_page_activation")
641  );
642 
643  if (!$active) {
644  $act_data = ilPageObject::_lookupActivationData((int) $a_node["child"], "lm");
645  if ($act_data["show_activation_info"] &&
646  (ilUtil::now() < $act_data["activation_start"])) {
647  return true;
648  } else {
649  return false;
650  }
651  } else {
652  return true;
653  }
654  }
655 }
static $instancesbyobj
static sortArray( $array, $a_array_sortby, $a_array_sortorder=0, $a_numeric=false, $a_keep_keys=false)
sortArray
static _lookupName($a_user_id)
lookup user name
static _recordReadEvent( $a_type, $a_ref_id, $obj_id, $usr_id, $isCatchupWriteEvents=true, $a_ext_rc=false, $a_ext_time=false)
Records a read event and catches up with write events.
Track access to ILIAS learning modules.
const IL_CAL_DATETIME
hasPredIncorrectAnswers($a_obj_id, $a_ignore_unlock=false)
Has predecessing incorrect answers.
global $DIC
Definition: saml.php:7
trackLastPageAccess($usr_id, $lm_id, $obj_id)
Track last accessed page for a learning module.
static _updateStatus($a_obj_id, $a_usr_id, $a_obj=null, $a_percentage=false, $a_force_raise=false)
Update status.
getBlockedUsersInformation()
Get blocked users information.
setCurrentPage($a_val)
Set current page.
static _getAllLMObjectsOfLM($a_lm_id, $a_type="")
Get all objects of learning module.
static _tracProgress($a_user_id, $a_obj_id, $a_ref_id, $a_obj_type='')
getIconForLMObject($a_node, $a_highlighted_node=0)
Get icon for lm object.
const IL_CAL_UNIX
static _lookupTitle($a_obj_id)
Lookup title.
static getInstanceByObjId($a_obj_id, $a_user_id=0)
Get instance.
static now()
Return current timestamp in Y-m-d H:i:s format.
user()
Definition: user.php:4
getCurrentPage()
Get current page.
Base exception class for learning module presentation.
trackPageAndChapterAccess($a_page_id)
Track page and chapter access.
$time_diff
Definition: langcheck.php:760
static getAnswerStatus($a_q_id, $a_user_id=0)
Get statistics for question.
getAllQuestionsCorrect()
Have all questoins been answered correctly (and questions exist)?
foreach($_POST as $key=> $value) $res
static _lookupActive($a_id, $a_parent_type, $a_check_scheduled_activation=false, $a_lang="-")
lookup activation status
static getImagePath($img, $module_path="", $mode="output", $offline=false)
get image path (for images located in a template directory)
static _lookupObjId($a_id)
static getInstance($a_tree_id)
Get Instance.
$as
static queryQuestionsOfLearningModule( $a_lm_id, $a_order_field, $a_order_dir, $a_offset, $a_limit)
Get questions of learning module.
Date and time handling
$ilUser
Definition: imgupload.php:18
determineProgressStatus($a_obj_id, &$a_has_pred_incorrect_answers, &$a_has_pred_incorrect_not_unlocked_answers)
Determine progress status of nodes.
$row
trackAccess($a_page_id, $user_id)
Track access to lm page.
static getInstance($a_ref_id, $a_user_id=0)
Get instance.
static _lookupActivationData($a_id, $a_parent_type, $a_lang="-")
Lookup activation data.
$lm_set
global $ilDB
$ret
Definition: parser.php:6
__construct($a_id, $a_by_obj_id=false, $a_user_id)
Constructor.
static _isNodeVisible($a_node)
Is node visible for the learner.
loadLMTrackingData()
Load LM tracking data.