ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
AdaptiveNavigator.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
32
37{
42
46 protected array $conditions_cache = [];
50 protected array $edge_targets_cache = [];
54 protected array $dependency_targets_cache = [];
66 protected int $max_points_budget = 0;
70 protected array $can_leave_cache = [];
74 protected array $can_enter_ignoring_edges_cache = [];
75
76 public function __construct(
78 ) {
79 $this->condition_factory = $condition_factory;
80 }
81
87 public function preload(array $items): void
88 {
89 $ref_ids = [];
90 foreach ($items as $item) {
91 $ref_ids[] = $item->getRefId();
92 }
93 $ref_ids = array_values(array_unique($ref_ids));
94 $missing_ref_ids = array_values(array_filter(
95 array_unique($ref_ids),
96 fn(int $ref_id): bool => !isset($this->conditions_cache[$ref_id])
97 ));
98
99 $this->edge_targets_cache = [];
100 $this->dependency_targets_cache = [];
101 $this->points_structural_successors_cache = [];
102 $this->points_structural_predecessors_cache = [];
103 $this->can_leave_cache = [];
104 $this->can_enter_ignoring_edges_cache = [];
105
106 if ($ref_ids === []) {
107 return;
108 }
109
110 if ($missing_ref_ids !== []) {
111 $ids_per_item = $this->condition_factory->preloadConditionIdsForItems($missing_ref_ids);
112
113 foreach ($ids_per_item as $ref_id => $ids) {
114 $conditions = [];
115 foreach ($ids as $id) {
116 try {
117 $conditions[] = $this->condition_factory->getConditionInstanceById($id);
118 } catch (\Throwable $t) {
119 continue;
120 }
121 }
122 $this->conditions_cache[$ref_id] = $conditions;
123 }
124 }
125
126 $this->buildNavigationSourceCaches($ref_ids);
127 $this->buildPointsNavigationCaches($items);
128 }
129
136 public function getSuccessors(array $items, \LSLearnerItem $current): array
137 {
138 $successors = [];
139 foreach ($items as $item) {
140 if ($item->getRefId() === $current->getRefId()) {
141 continue;
142 }
143 if (
144 $this->isEdge($current->getRefId(), $item->getRefId())
145 || in_array($current->getRefId(), $this->getDependencyTargetsFor($item->getRefId()), true)
146 ) {
147 if ($this->canEnterFrom($current, $item)) {
148 $successors[$item->getRefId()] = $item;
149 }
150 continue;
151 }
152
153 if ($this->isGlobalSuccessorTarget($item->getRefId()) && $this->canEnter($item)) {
154 $successors[$item->getRefId()] = $item;
155 }
156 }
157 return array_values($successors);
158 }
159
166 public function getStructuralSuccessors(array $items, \LSItem $current): array
167 {
168 $successors = [];
169 foreach ($items as $item) {
170 if ($item->getRefId() === $current->getRefId()) {
171 continue;
172 }
173 if (
174 $this->isEdge($current->getRefId(), $item->getRefId())
175 || in_array($current->getRefId(), $this->getDependencyTargetsFor($item->getRefId()), true)
176 || in_array($item->getRefId(), $this->getPointsStructuralSuccessorsFor($current->getRefId()), true)
177 ) {
178 $successors[] = $item;
179 }
180 }
181 return $successors;
182 }
183
190 public function getPredecessors(array $items, \LSLearnerItem $current): array
191 {
192 $predecessor_ref_ids = array_values(array_unique(array_merge(
193 $this->getEdgeTargetsFor($current->getRefId()),
194 $this->getDependencyTargetsFor($current->getRefId()),
195 $this->getPointsStructuralPredecessorsFor($current->getRefId())
196 )));
197 $predecessors = [];
198 foreach ($items as $item) {
199 if (in_array($item->getRefId(), $predecessor_ref_ids, true)) {
200 $predecessors[] = $item;
201 }
202 }
203 return $predecessors;
204 }
205
209 public function canLeave(\LSLearnerItem $current): bool
210 {
211 $ref_id = $current->getRefId();
212 if (isset($this->can_leave_cache[$ref_id])) {
213 return $this->can_leave_cache[$ref_id];
214 }
215
216 $can_leave = true;
217 foreach ($this->getConditionsFor($ref_id) as $condition) {
218 if ($condition instanceof OutputConditionInterface && !$this->checkCondition($condition)) {
219 $can_leave = false;
220 break;
221 }
222 }
223
224 $this->can_leave_cache[$ref_id] = $can_leave;
225 return $can_leave;
226 }
227
231 public function canEnter(\LSLearnerItem $target): bool
232 {
233 foreach ($this->getConditionsFor($target->getRefId()) as $condition) {
234 if ($condition instanceof InputConditionInterface && !$this->checkCondition($condition)) {
235 return false;
236 }
237 }
238 return true;
239 }
240
245 {
246 $ref_id = $target->getRefId();
247 if (isset($this->can_enter_ignoring_edges_cache[$ref_id])) {
248 return $this->can_enter_ignoring_edges_cache[$ref_id];
249 }
250
251 $can_enter = true;
252 foreach ($this->getConditionsFor($ref_id) as $condition) {
253 if ($this->isAlternativeEdgeCondition($condition)) {
254 continue;
255 }
256 if ($condition instanceof InputConditionInterface && !$this->checkCondition($condition)) {
257 $can_enter = false;
258 break;
259 }
260 }
261
262 $this->can_enter_ignoring_edges_cache[$ref_id] = $can_enter;
263 return $can_enter;
264 }
265
269 public function canEnterFrom(\LSLearnerItem $current, \LSLearnerItem $target): bool
270 {
271 $conditions = $this->getConditionsFor($target->getRefId());
272 $has_alternative_edge_condition = array_any(
273 $conditions,
274 fn(AbstractCondition $condition): bool => $this->isAlternativeEdgeCondition($condition)
275 );
276 $matches_alternative_edge_condition = false;
277 $has_dependency_condition = false;
278 $matches_dependency_condition = false;
279
280 foreach ($conditions as $condition) {
281 if (!$condition instanceof InputConditionInterface) {
282 continue;
283 }
284
285 if ($this->isAlternativeEdgeCondition($condition)) {
286 if (!$this->conditionReferencesCurrent($condition, $current->getRefId())) {
287 continue;
288 }
289 $matches_alternative_edge_condition = true;
290 if (!$this->checkCondition($condition)) {
291 return false;
292 }
293 continue;
294 }
295
296 if ($this->isDependencyCondition($condition)) {
297 $has_dependency_condition = true;
298 if ($this->conditionReferencesCurrent($condition, $current->getRefId())) {
299 $matches_dependency_condition = true;
300 }
301 }
302
303 if (!$this->checkCondition($condition)) {
304 return false;
305 }
306 }
307
308 if ($has_alternative_edge_condition) {
309 return $matches_alternative_edge_condition;
310 }
311
312 if ($has_dependency_condition && !$matches_dependency_condition) {
313 return false;
314 }
315
316 return true;
317 }
318
324 public function getInputConditionIds(\LSLearnerItem $item): array
325 {
326 $ids = [];
327 foreach ($this->getConditionsFor($item->getRefId()) as $condition) {
328 if ($condition instanceof InputConditionInterface) {
329 $id = $condition->getConditionId();
330 if ($id !== null) {
331 $ids[] = $id;
332 }
333 }
334 }
335 return $ids;
336 }
337
343 public function getOutputConditionIds(\LSLearnerItem $item): array
344 {
345 $ids = [];
346 foreach ($this->getConditionsFor($item->getRefId()) as $condition) {
347 if ($condition instanceof OutputConditionInterface) {
348 $id = $condition->getConditionId();
349 if ($id !== null) {
350 $ids[] = $id;
351 }
352 }
353 }
354 return $ids;
355 }
356
360 public function requiresVisitForCompletion(\LSLearnerItem $item): bool
361 {
362 $has_output_condition = false;
363 foreach ($this->getConditionsFor($item->getRefId()) as $condition) {
364 if (!$condition instanceof OutputConditionInterface) {
365 continue;
366 }
367 $has_output_condition = true;
368 if ($condition instanceof AlwaysOutputCondition) {
369 return true;
370 }
371 }
372 return !$has_output_condition;
373 }
374
381 public function getPointsOutputRefIds(array $items): array
382 {
383 $ref_ids = [];
384 foreach ($items as $item) {
385 foreach ($this->getConditionsFor($item->getRefId()) as $condition) {
386 if (
387 $condition instanceof AccruedValueOutputConditionInterface
388 && $condition->getAccumulationIdentifier() === 'points'
389 ) {
390 $ref_ids[] = $item->getRefId();
391 break;
392 }
393 }
394 }
395
396 return array_values(array_unique($ref_ids));
397 }
398
402 public function getInputConditions(\LSItem $item): array
403 {
404 return array_values(array_filter(
405 $this->getConditionsFor($item->getRefId()),
406 static fn(AbstractCondition $condition): bool => $condition instanceof InputConditionInterface
407 ));
408 }
409
413 protected function checkCondition(AbstractCondition $condition): bool
414 {
415 try {
416 return $condition->check();
417 } catch (\Throwable $t) {
418 return false;
419 }
420 }
421
425 protected function isEdge(int $from_ref_id, int $to_ref_id): bool
426 {
427 return in_array($from_ref_id, $this->getEdgeTargetsFor($to_ref_id), true);
428 }
429
435 protected function getEdgeTargetsFor(int $item_ref_id): array
436 {
437 if (isset($this->edge_targets_cache[$item_ref_id])) {
438 return $this->edge_targets_cache[$item_ref_id];
439 }
440
441 return $this->edge_targets_cache[$item_ref_id] = [];
442 }
443
447 protected function getDependencyTargetsFor(int $item_ref_id): array
448 {
449 return $this->dependency_targets_cache[$item_ref_id] ?? [];
450 }
451
455 protected function buildNavigationSourceCaches(array $item_ref_ids): void
456 {
457 $this->edge_targets_cache = [];
458 $this->dependency_targets_cache = [];
459
460 foreach ($item_ref_ids as $item_ref_id) {
461 $edge_targets = [];
462 $dependency_targets = [];
463
464 foreach ($this->getConditionsFor($item_ref_id) as $condition) {
465 if (!$condition instanceof InputConditionNavigationAwareInterface) {
466 continue;
467 }
468
469 $source_ref_ids = array_values(array_unique(array_map('intval', $condition->getNavigationSourceRefIds())));
470 if ($source_ref_ids === []) {
471 continue;
472 }
473
474 if ($condition->getNavigationMode() === InputConditionNavigationAwareInterface::NAVIGATION_MODE_EDGE) {
475 $edge_targets = array_values(array_unique(array_merge($edge_targets, $source_ref_ids)));
476 continue;
477 }
478
479 if ($condition->getNavigationMode() === InputConditionNavigationAwareInterface::NAVIGATION_MODE_DEPENDENCY) {
480 $dependency_targets = array_values(array_unique(array_merge($dependency_targets, $source_ref_ids)));
481 }
482 }
483
484 $this->edge_targets_cache[$item_ref_id] = $edge_targets;
485 $this->dependency_targets_cache[$item_ref_id] = $dependency_targets;
486 }
487 }
488
494 protected function buildPointsNavigationCaches(array $items): void
495 {
496 $this->points_structural_successors_cache = [];
497 $this->points_structural_predecessors_cache = [];
498 $this->max_points_budget = 0;
499
500 foreach ($items as $item) {
501 $ref_id = $item->getRefId();
502 $this->points_structural_successors_cache[$ref_id] = [];
503 $this->points_structural_predecessors_cache[$ref_id] = [];
504 $this->max_points_budget += $this->getConfiguredPointsOutput($ref_id);
505 }
506
507 if ($items === []) {
508 return;
509 }
510
511 $queue = [];
512 $seen_states = [];
513 foreach ($items as $item) {
514 $ref_id = $item->getRefId();
515 if ($this->hasSourceScopedNavigation($ref_id)) {
516 continue;
517 }
518 if (!$this->canStructurallyEnterWithAvailablePoints($ref_id, 0)) {
519 continue;
520 }
521
522 $points_after = $this->getConfiguredPointsOutput($ref_id);
523 $seen_states[$ref_id][$points_after] = true;
524 $queue[] = [$ref_id, $points_after];
525 }
526
527 while ($queue !== []) {
528 [$current_ref_id, $current_points_after] = array_shift($queue);
529 $current_points_before = max(0, $current_points_after - $this->getConfiguredPointsOutput($current_ref_id));
530
531 foreach ($items as $target) {
532 $target_ref_id = $target->getRefId();
533 if ($target_ref_id === $current_ref_id) {
534 continue;
535 }
536
537 if ($this->isEdge($current_ref_id, $target_ref_id)) {
538 if ($this->canStructurallyEnterWithAvailablePoints($target_ref_id, $current_points_after)) {
539 $candidate_points = min(
540 $this->max_points_budget,
541 $current_points_after + $this->getConfiguredPointsOutput($target_ref_id)
542 );
543 if (!isset($seen_states[$target_ref_id][$candidate_points])) {
544 $seen_states[$target_ref_id][$candidate_points] = true;
545 $queue[] = [$target_ref_id, $candidate_points];
546 }
547 }
548 continue;
549 }
550
551 $required_points = $this->getPureAccumulatedRequirement($target_ref_id, 'points');
552 if ($required_points === null || $required_points === 0) {
553 continue;
554 }
555 if ($current_points_before >= $required_points || $current_points_after < $required_points) {
556 continue;
557 }
558
559 $this->addPointsStructuralEdge($current_ref_id, $target_ref_id);
560
561 $candidate_points = min(
562 $this->max_points_budget,
563 $current_points_after + $this->getConfiguredPointsOutput($target_ref_id)
564 );
565 if (!isset($seen_states[$target_ref_id][$candidate_points])) {
566 $seen_states[$target_ref_id][$candidate_points] = true;
567 $queue[] = [$target_ref_id, $candidate_points];
568 }
569 }
570 }
571 }
572
573 protected function addPointsStructuralEdge(int $from_ref_id, int $to_ref_id): void
574 {
575 if (!in_array($to_ref_id, $this->points_structural_successors_cache[$from_ref_id] ?? [], true)) {
576 $this->points_structural_successors_cache[$from_ref_id][] = $to_ref_id;
577 }
578 if (!in_array($from_ref_id, $this->points_structural_predecessors_cache[$to_ref_id] ?? [], true)) {
579 $this->points_structural_predecessors_cache[$to_ref_id][] = $from_ref_id;
580 }
581 }
582
586 protected function isGlobalSuccessorTarget(int $target_ref_id): bool
587 {
588 foreach ($this->getConditionsFor($target_ref_id) as $condition) {
589 if (
590 $condition instanceof InputConditionNavigationAwareInterface
592 ) {
593 return !$this->hasSourceScopedNavigation($target_ref_id);
594 }
595 }
596
597 return false;
598 }
599
603 protected function canStructurallyEnterWithAvailablePoints(int $target_ref_id, int $available_points): bool
604 {
605 $required_points = $this->getPureAccumulatedRequirement($target_ref_id, 'points');
606 if ($required_points === null) {
607 return false;
608 }
609
610 return $available_points >= $required_points;
611 }
612
616 protected function getPureAccumulatedRequirement(int $target_ref_id, string $accumulation_identifier): ?int
617 {
618 $required_points = 0;
619 foreach ($this->getConditionsFor($target_ref_id) as $condition) {
620 if (
621 $condition instanceof InputConditionNavigationAwareInterface
623 ) {
624 return null;
625 }
626 if (
627 $condition instanceof AccruedValueInputConditionInterface
628 && $condition->getAccumulationIdentifier() === $accumulation_identifier
629 ) {
630 $required_points = max($required_points, $condition->getRequiredAccumulatedValue());
631 continue;
632 }
633 if ($condition instanceof InputConditionInterface) {
634 return null;
635 }
636 }
637
638 return $required_points;
639 }
640
641 protected function getConfiguredPointsOutput(int $item_ref_id): int
642 {
643 $points = 0;
644 foreach ($this->getConditionsFor($item_ref_id) as $condition) {
645 if (
646 $condition instanceof AccruedValueOutputConditionInterface
647 && $condition->getAccumulationIdentifier() === 'points'
648 ) {
649 try {
650 $points += $condition->getAccumulatedValue();
651 } catch (\Throwable $t) {
652 continue;
653 }
654 }
655 }
656
657 return $points;
658 }
659
663 protected function getPointsStructuralSuccessorsFor(int $item_ref_id): array
664 {
665 return $this->points_structural_successors_cache[$item_ref_id] ?? [];
666 }
667
671 protected function getPointsStructuralPredecessorsFor(int $item_ref_id): array
672 {
673 return $this->points_structural_predecessors_cache[$item_ref_id] ?? [];
674 }
675
676 protected function isAlternativeEdgeCondition(AbstractCondition $condition): bool
677 {
678 return $condition instanceof InputConditionNavigationAwareInterface
679 && $condition->getNavigationMode() === InputConditionNavigationAwareInterface::NAVIGATION_MODE_EDGE;
680 }
681
682 protected function isDependencyCondition(AbstractCondition $condition): bool
683 {
684 return $condition instanceof InputConditionNavigationAwareInterface
685 && $condition->getNavigationMode() === InputConditionNavigationAwareInterface::NAVIGATION_MODE_DEPENDENCY;
686 }
687
688 protected function conditionReferencesCurrent(AbstractCondition $condition, int $current_ref_id): bool
689 {
690 return $condition instanceof InputConditionNavigationAwareInterface
691 && in_array($current_ref_id, $condition->getNavigationSourceRefIds(), true);
692 }
693
694 protected function hasSourceScopedNavigation(int $target_ref_id): bool
695 {
696 return $this->getEdgeTargetsFor($target_ref_id) !== [] || $this->getDependencyTargetsFor($target_ref_id) !== [];
697 }
698
704 protected function getConditionsFor(int $item_ref_id): array
705 {
706 if (isset($this->conditions_cache[$item_ref_id])) {
707 return $this->conditions_cache[$item_ref_id];
708 }
709
710 $conditions = [];
711 foreach ($this->condition_factory->getConditionIdsForItem($item_ref_id) as $condition_id) {
712 try {
713 $conditions[] = $this->condition_factory->getConditionInstanceById($condition_id);
714 } catch (\Throwable $t) {
715 continue;
716 }
717 }
718
719 $this->conditions_cache[$item_ref_id] = $conditions;
720 return $conditions;
721 }
722}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
check()
Checks if the condition is fulfilled.
Provides condition-based navigation through an adaptive learning sequence.
isEdge(int $from_ref_id, int $to_ref_id)
Determines whether an edge connects two items.
buildPointsNavigationCaches(array $items)
Precomputes synthetic points-based unlock edges for map rendering.
canLeave(\LSLearnerItem $current)
Determines whether all output conditions of the current item are met.
canEnterFrom(\LSLearnerItem $current, \LSLearnerItem $target)
Determines whether the target is enterable from the current item.
preload(array $items)
Preloads condition instances for the given items.
getOutputConditionIds(\LSLearnerItem $item)
Returns the identifiers of output conditions assigned to an item.
getInputConditionIds(\LSLearnerItem $item)
Returns the identifiers of input conditions assigned to an item.
isAlternativeEdgeCondition(AbstractCondition $condition)
canEnter(\LSLearnerItem $target)
Determines whether all input conditions of the target item are met.
checkCondition(AbstractCondition $condition)
Evaluates a condition and treats evaluation failures as unmet conditions.
requiresVisitForCompletion(\LSLearnerItem $item)
Determines whether completion is only proven by visiting the item.
isDependencyCondition(AbstractCondition $condition)
isGlobalSuccessorTarget(int $target_ref_id)
Returns whether the target behaves like a global points gate in the player.
getPointsOutputRefIds(array $items)
Returns the reference identifiers of items with a configured points output condition.
getPredecessors(array $items, \LSLearnerItem $current)
Returns the items that are connected to the current item by an incoming edge.
getEdgeTargetsFor(int $item_ref_id)
Returns the source reference identifiers of edges leading to an item.
getConditionsFor(int $item_ref_id)
Returns the condition instances assigned to an item.
getSuccessors(array $items, \LSLearnerItem $current)
Returns the items connected to and enterable from the current item.
conditionReferencesCurrent(AbstractCondition $condition, int $current_ref_id)
__construct(ConditionFactory $condition_factory)
addPointsStructuralEdge(int $from_ref_id, int $to_ref_id)
getStructuralSuccessors(array $items, \LSItem $current)
Returns the items structurally connected to the current item.
canStructurallyEnterWithAvailablePoints(int $target_ref_id, int $available_points)
Returns whether the item can be entered using points-only structural data.
canEnterIgnoringEdges(\LSLearnerItem $target)
Determines whether the target is enterable without learning-progress edge conditions.
getPureAccumulatedRequirement(int $target_ref_id, string $accumulation_identifier)
Returns the required points if the item's non-edge inputs are points-only.
ConditionFactory $condition_factory
Creates condition instances from their identifiers.
Data holding class LSItem .
Definition: LSItem.php:25
getRefId()
Definition: LSItem.php:93
Add learning progress and availability information to the LSItem.
Marker interface for input conditions in the Learning Sequence condition system.
Marker interface for output conditions in the Learning Sequence condition system.
Abstraction of the navigation between the objects of a learning sequence.
Definition: LSNavigator.php:31