19declare(strict_types=1);
100 $local_dic = QuestionPoolDIC::dic();
101 $this->request = $local_dic[
'request_data_collector'];
126 if ($this->title !==
''
129 && $this->image_filename
130 && $this->answers !== []
148 $this->db->manipulateF(
149 'DELETE FROM qpl_a_imagemap WHERE question_fi = %s',
155 foreach ($this->answers as $key => $value) {
156 $answer_obj = $this->answers[$key];
157 $answer_obj->setOrder($key);
158 $next_id = $this->db->nextId(
'qpl_a_imagemap');
159 $this->db->manipulateF(
160 'INSERT INTO qpl_a_imagemap (answer_id, question_fi, answertext, points, aorder, coords, area, points_unchecked) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)',
161 [
'integer',
'integer',
'text',
'float',
'integer',
'text',
'text',
'float' ],
162 [ $next_id, $this->
id, $answer_obj->getAnswertext(
163 ), $answer_obj->getPoints(), $answer_obj->getOrder(
164 ), $answer_obj->getCoords(), $answer_obj->getArea(
165 ), $answer_obj->getPointsUnchecked() ]
172 $this->db->manipulateF(
178 $this->db->manipulateF(
180 ) .
' (question_fi, image_file, is_multiple_choice) VALUES (%s, %s, %s)',
181 [
'integer',
'text',
'integer' ],
184 $this->image_filename,
185 (
int) $this->is_multiple_choice
198 int $source_question_id,
199 int $source_parent_id,
200 int $target_question_id,
201 int $target_parent_id
203 $image_source_path = $this->getImagePath($source_question_id, $source_parent_id);
204 $image_target_path = $this->getImagePath($target_question_id, $target_parent_id);
206 if (!file_exists($image_target_path)) {
209 $this->removeAllImageFiles($image_target_path);
212 if (!file_exists($image_source_path) || !is_readable($image_source_path)) {
213 $this->log->forComponent(
'qpl')->alert(
214 "Could not copy imagemap question files: Source path '{$image_source_path}' does not exist or is not readable.",
216 'source_question_id' => $source_question_id,
217 'source_parent_id' => $source_parent_id,
218 'target_question_id' => $target_question_id,
219 'target_parent_id' => $target_parent_id,
220 'image_source_path' => $image_source_path,
221 'image_target_path' => $image_target_path
227 $src = opendir($image_source_path);
228 while ($src_file = readdir($src)) {
229 if ($src_file ===
'.' || $src_file ===
'..') {
233 $image_source_path . DIRECTORY_SEPARATOR . $src_file,
234 $image_target_path . DIRECTORY_SEPARATOR . $src_file
241 $result = $this->db->queryF(
242 'SELECT qpl_questions.*, ' . $this->getAdditionalTableName() .
'.* FROM qpl_questions LEFT JOIN ' . $this->getAdditionalTableName() .
' ON ' . $this->getAdditionalTableName() .
'.question_fi = qpl_questions.question_id WHERE qpl_questions.question_id = %s',
246 if ($result->numRows() == 1) {
247 $data = $this->db->fetchAssoc($result);
248 $this->setId($question_id);
249 $this->setObjId(
$data[
'obj_fi']);
250 $this->setTitle((
string)
$data[
'title']);
251 $this->setComment((
string)
$data[
'description']);
252 $this->setOriginalId(
$data[
'original_id']);
253 $this->setNrOfTries(
$data[
'nr_of_tries']);
254 $this->setAuthor(
$data[
'author']);
255 $this->setPoints(
$data[
'points']);
256 $this->setOwner(
$data[
'owner']);
257 $this->setIsMultipleChoice(
$data[
'is_multiple_choice'] == self::MODE_MULTIPLE_CHOICE);
259 $this->setImageFilename(
$data[
'image_file'] ??
'');
268 $this->setAdditionalContentEditingMode(
$data[
'add_cont_edit_mode']);
272 $result = $this->db->queryF(
273 'SELECT * FROM qpl_a_imagemap WHERE question_fi = %s ORDER BY aorder ASC',
277 if ($result->numRows() > 0) {
278 while (
$data = $this->db->fetchAssoc($result)) {
280 $image_map_question->setCoords(
$data[
'coords']);
281 $image_map_question->setArea(
$data[
'area']);
282 $image_map_question->setPointsUnchecked(
$data[
'points_unchecked']);
283 array_push($this->answers, $image_map_question);
287 parent::loadFromDb($question_id);
294 if (count($shapes) > 0) {
295 foreach ($shapes as $shape) {
296 $this->addAnswer($shape->getAnswertext(), 0.0, count($this->answers), $shape->getCoords(), $shape->getArea());
305 string $upload_file_name,
306 string $upload_tmp_name
308 $path_parts = pathinfo($upload_file_name);
309 $suffix = $path_parts[
'extension'] ??
'';
311 if (!in_array($path_parts[
'extension'], [
'jpg',
'jpeg',
'png',
'gif'])) {
312 $this->tpl->setOnScreenMessage(
'failure',
'The image could not be uploaded!');
316 $image_filename = bin2hex(random_bytes(10)) .
".{$suffix}";
318 $imagepath = $this->getImagePath();
319 if (!file_exists($imagepath)) {
324 $this->tpl->setOnScreenMessage(
'failure',
'The image could not be uploaded!');
328 $this->setImageFilename($image_filename);
333 return $this->image_filename;
337 string $image_filename
339 $this->image_filename = $image_filename;
344 $imagemap_contents =
'<map name=\'' . $this->title .
'\'>
';
345 for ($i = 0; $i < count($this->answers); $i++) {
346 $imagemap_contents .= '<area alt=\
'' . $this->answers[$i]->getAnswertext() .
'\' ';
347 $imagemap_contents .= 'shape=\
'' . $this->answers[$i]->getArea() .
'\' ';
348 $imagemap_contents .= 'coords=\
'' . $this->answers[$i]->getCoords() .
'\' ';
349 $imagemap_contents .= "href=\"{$href}&selimage=" . $this->answers[$i]->getOrder() . "\" /> ";
351 $imagemap_contents .= '</map>
';
352 return $imagemap_contents;
355 public function addAnswer(
356 string $answertext = '',
361 float $points_unchecked = 0.0
363 if (array_key_exists($order, $this->answers)) {
365 $answer = new ASS_AnswerImagemap($answertext, $points, $order, 0, -1);
366 $answer->setCoords($coords);
367 $answer->setArea($area);
368 $answer->setPointsUnchecked($points_unchecked);
369 for ($i = count($this->answers) - 1; $i >= $order; $i--) {
370 $this->answers[$i + 1] = $this->answers[$i];
371 $this->answers[$i + 1]->setOrder($i + 1);
373 $this->answers[$order] = $answer;
376 $answer = new ASS_AnswerImagemap($answertext, $points, count($this->answers), 0, -1);
377 $answer->setCoords($coords);
378 $answer->setArea($area);
379 $answer->setPointsUnchecked($points_unchecked);
380 array_push($this->answers, $answer);
384 public function getAnswerCount(): int
386 return count($this->answers);
389 public function getAnswer(int $index = 0): ?ASS_AnswerImagemap
394 if (count($this->answers) < 1) {
397 if ($index >= count($this->answers)) {
400 return $this->answers[$index];
403 public function &getAnswers(): array
405 return $this->answers;
408 public function deleteArea(int $index = 0): void
413 if (count($this->answers) < 1) {
416 if ($index >= count($this->answers)) {
419 unset($this->answers[$index]);
420 $this->answers = array_values($this->answers);
421 for ($i = 0; $i < count($this->answers); $i++) {
422 if ($this->answers[$i]->getOrder() > $index) {
423 $this->answers[$i]->setOrder($i);
428 public function flushAnswers(): void
433 public function getMaximumPoints(): float
436 foreach ($this->answers as $key => $value) {
437 if ($this->is_multiple_choice) {
438 if ($value->getPoints() > $value->getPointsUnchecked()) {
439 $points += $value->getPoints();
441 $points += $value->getPointsUnchecked();
444 if ($value->getPoints() > $points) {
445 $points = $value->getPoints();
452 public function calculateReachedPoints(
455 bool $authorized_solution = true
457 if ($pass === null) {
458 $pass = $this->getSolutionMaxPass($active_id);
460 $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorized_solution);
462 while ($data = $this->db->fetchAssoc($result)) {
463 if ($data['value1
'] !== '') {
464 $found_values[] = $data['value1
'];
468 $points = $this->calculateReachedPointsForSolution($found_values);
473 public function calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $preview_session): float
475 $solution_data = $preview_session->getParticipantsSolution();
477 $reached_points = $this->calculateReachedPointsForSolution(is_array($solution_data) ? array_values($solution_data) : []);
479 return $this->ensureNonNegativePoints($reached_points);
490 public function saveWorkingData(
493 bool $authorized = true
495 if (is_null($pass)) {
496 $pass = ilObjTest::_getPass($active_id);
499 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
500 function () use ($active_id, $pass, $authorized) {
502 // remove the dummy record of the intermediate solution
503 $this->deleteDummySolutionRecord($active_id, $pass);
505 // delete the authorized solution and make the intermediate solution authorized (keeping timestamps)
506 $this->removeCurrentSolution($active_id, $pass, true);
507 $this->updateCurrentSolutionsAuthorization($active_id, $pass, true, true);
511 $this->forceExistingIntermediateSolution(
514 $this->is_multiple_choice
517 if ($this->isReuseSolutionSelectionRequest()) {
518 $selection = $this->getReuseSolutionSelectionParameter();
520 foreach ($selection as $selectedIndex) {
521 $this->saveCurrentSolution($active_id, $pass, (int) $selectedIndex, null, $authorized);
526 if ($this->isRemoveSolutionSelectionRequest()) {
527 $selection = $this->getRemoveSolutionSelectionParameter();
528 $this->deleteSolutionRecordByValues($active_id, $pass, $authorized, [
529 'value1
' => (int) $selection
534 if (!$this->isAddSolutionSelectionRequest()) {
537 $selection = $this->getAddSolutionSelectionParameter();
539 if ($this->is_multiple_choice) {
540 $this->deleteSolutionRecordByValues($active_id, $pass, $authorized, [
541 'value1
' => (int) $this->request->raw('selImage
')
544 $this->removeCurrentSolution($active_id, $pass, $authorized);
547 $this->saveCurrentSolution($active_id, $pass, $this->request->raw('selImage
'), null, $authorized);
554 protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
556 $solution = $previewSession->getParticipantsSolution();
558 if ($this->is_multiple_choice
559 && $this->request->isset('remImage
')) {
560 unset($solution[$this->request->int('remImage
')]);
563 if ($this->request->isset('selImage
')) {
564 if (!$this->is_multiple_choice) {
568 $solution[$this->request->int('selImage
')] = $this->request->int('selImage
');
571 $previewSession->setParticipantsSolution($solution);
582 public function getQuestionType(): string
595 public function getAdditionalTableName(): string
597 return 'qpl_qst_imagemap
';
608 public function getAnswerTableName(): string
610 return 'qpl_a_imagemap
';
617 public function getRTETextWithMediaObjects(): string
619 $text = parent::getRTETextWithMediaObjects();
620 foreach ($this->answers as $index => $answer) {
621 $text .= $this->feedbackOBJ->getSpecificAnswerFeedbackContent($this->getId(), 0, $index);
629 public function deleteImage(): void
631 $file = $this->getImagePath() . $this->getImageFilename();
633 $this->flushAnswers();
634 $this->image_filename = '';
640 public function toJSON(): string
643 $result['id'] = $this->getId();
644 $result['type
'] = (string) $this->getQuestionType();
645 $result['title
'] = $this->getTitleForHTMLOutput();
646 $result['question
'] = $this->formatSAQuestion($this->getQuestion());
647 $result['nr_of_tries
'] = $this->getNrOfTries();
648 $result['shuffle
'] = $this->getShuffle();
649 $result['is_multiple
'] = $this->getIsMultipleChoice();
650 $result['feedback
'] = [
651 'onenotcorrect
' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
652 'allcorrect
' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
654 $result['image
'] = $this->getImagePathWeb() . $this->getImageFilename();
658 foreach ($this->getAnswers() as $key => $answer_obj) {
659 array_push($answers, [
660 'answertext
' => (string) $answer_obj->getAnswertext(),
661 'points
' => (float) $answer_obj->getPoints(),
662 'points_unchecked
' => (float) $answer_obj->getPointsUnchecked(),
664 'coords
' => $answer_obj->getCoords(),
665 'state
' => $answer_obj->getState(),
666 'area
' => $answer_obj->getArea(),
667 'feedback
' => $this->formatSAQuestion(
668 $this->feedbackOBJ->getSpecificAnswerFeedbackExportPresentation($this->getId(), 0, $key)
673 $result['answers
'] = $answers;
675 $mobs = ilObjMediaObject::_getMobsOfObject('qpl:
html', $this->getId());
676 $result['mobs
'] = $mobs;
678 return json_encode($result);
681 protected function calculateReachedPointsForSolution(?array $found_values): float
683 if ($found_values === null) {
687 if (count($found_values) > 0) {
688 foreach ($this->answers as $key => $answer) {
689 if (in_array($key, $found_values)) {
690 $points += $answer->getPoints();
691 } elseif ($this->getIsMultipleChoice()) {
692 $points += $answer->getPointsUnchecked();
700 public function getOperators(string $expression): array
702 return ilOperatorsExpressionMapping::getOperatorsByExpression($expression);
705 public function getExpressionTypes(): array
708 iQuestionCondition::PercentageResultExpression,
709 iQuestionCondition::NumberOfResultExpression,
710 iQuestionCondition::EmptyAnswerExpression,
711 iQuestionCondition::ExclusiveResultExpression
715 public function getUserQuestionResult(
718 ): ilUserQuestionResult {
719 $result = new ilUserQuestionResult($this, $active_id, $pass);
721 $maxStep = $this->lookupMaxStep($active_id, $pass);
723 $data = $this->db->queryF(
724 'SELECT value1+1 as value1 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s
',
725 ['integer
', 'integer
', 'integer
', 'integer
'],
726 [$active_id, $pass, $this->getId(), $maxStep]
729 $data = $this->db->queryF(
730 'SELECT value1+1 as value1 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step IS
NULL',
731 ['integer
', 'integer
', 'integer
'],
732 [$active_id, $pass, $this->getId()]
736 while ($row = $this->db->fetchAssoc($data)) {
737 $result->addKeyValue($row['value1
'], $row['value1
']);
740 $points = $this->calculateReachedPoints($active_id, $pass);
741 $max_points = $this->getMaximumPoints();
743 $result->setReachedPercentage(($points / $max_points) * 100);
755 public function getAvailableAnswerOptions($index = null)
757 if ($index !== null) {
758 return $this->getAnswer($index);
760 return $this->getAnswers();
764 public function getTestOutputSolutions($activeId, $pass): array
766 $solution = parent::getTestOutputSolutions($activeId, $pass);
768 $this->currentSolution = [];
769 foreach ($solution as $record) {
770 $this->currentSolution[] = $record['value1
'];
775 protected function getAddSolutionSelectionParameter()
777 if (!$this->isAddSolutionSelectionRequest()) {
781 return $this->request->raw('selImage
');
783 protected function isAddSolutionSelectionRequest(): bool
785 if (!$this->request->isset('selImage
')) {
789 if (!strlen($this->request->raw('selImage
'))) {
795 protected function getRemoveSolutionSelectionParameter()
797 if (!$this->isRemoveSolutionSelectionRequest()) {
801 return $this->request->raw('remImage
');
803 protected function isRemoveSolutionSelectionRequest(): bool
805 if (!$this->is_multiple_choice) {
809 if (!$this->request->isset('remImage
')) {
813 if (!strlen($this->request->raw('remImage
'))) {
819 protected function getReuseSolutionSelectionParameter(): ?array
821 if (!$this->isReuseSolutionSelectionRequest()) {
825 return assQuestion::explodeKeyValues($this->request->raw('reuseSelection
'));
827 protected function isReuseSolutionSelectionRequest(): bool
829 if (!$this->getTestPresentationConfig()->isPreviousPassSolutionReuseAllowed()) {
833 if (!$this->request->isset('reuseSelection
')) {
837 if (!strlen($this->request->raw('reuseSelection
'))) {
841 if (!preg_match('/\d(,\d)*/
', $this->request->raw('reuseSelection
'))) {
848 public function toLog(AdditionalInformationGenerator $additional_info): array
851 AdditionalInformationGenerator::KEY_QUESTION_TYPE => (string) $this->getQuestionType(),
852 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $this->getTitleForHTMLOutput(),
853 AdditionalInformationGenerator::KEY_QUESTION_TEXT => $this->formatSAQuestion($this->getQuestion()),
854 AdditionalInformationGenerator::KEY_QUESTION_SHUFFLE_ANSWER_OPTIONS => $additional_info
855 ->getTrueFalseTagForBool($this->getShuffle()),
856 AdditionalInformationGenerator::KEY_QUESTION_IMAGEMAP_MODE => $this->getIsMultipleChoice()
857 ? $additional_info->getTagForLangVar('tst_imap_qst_mode_mc
')
858 : $additional_info->getTagForLangVar('tst_imap_qst_mode_sc
'),
859 AdditionalInformationGenerator::KEY_QUESTION_IMAGEMAP_IMAGE => $this->getImagePathWeb() . $this->getImageFilename(),
860 AdditionalInformationGenerator::KEY_FEEDBACK => [
861 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_INCOMPLETE => $this->formatSAQuestion(
862 $this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)
864 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_COMPLETE => $this->formatSAQuestion(
865 $this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true)
872 foreach ($this->getAnswers() as $key => $answer_obj) {
873 array_push($answers, [
874 AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTION => (string) $answer_obj->getAnswertext(),
875 AdditionalInformationGenerator::KEY_QUESTION_POINTS_CHECKED => (float) $answer_obj->getPoints(),
876 AdditionalInformationGenerator::KEY_QUESTION_POINTS_UNCHECKED => (float) $answer_obj->getPointsUnchecked(),
877 AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTION_ORDER => $order,
878 AdditionalInformationGenerator::KEY_QUESTION_IMAGEMAP_ANSWER_OPTION_COORDS => $answer_obj->getCoords(),
879 AdditionalInformationGenerator::KEY_FEEDBACK => $this->formatSAQuestion(
880 $this->feedbackOBJ->getSpecificAnswerFeedbackExportPresentation($this->getId(), 0, $key)
885 $result[AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTIONS] = $answers;
890 protected function solutionValuesToLog(
891 AdditionalInformationGenerator $additional_info,
892 array $solution_values
894 $parsed_solution = [];
895 foreach ($this->getAnswers() as $id => $answer) {
896 $value = $additional_info->getTagForLangVar('unchecked');
897 foreach ($solution_values as $solution) {
898 if ($solution['value1
'] == $id) {
899 $value = $additional_info->getTagForLangVar('checked');
903 $parsed_solution["{$answer->getArea()}: {$answer->getCoords()}"] = $value;
905 return $parsed_solution;
908 public function solutionValuesToText(array $solution_values): array
910 $parsed_solution = [];
911 foreach ($this->getAnswers() as $id => $answer) {
913 foreach ($solution_values as $solution) {
914 if ($solution['value1
'] == $id) {
915 $value = $this->lng->txt('checked');
919 $parsed_solution[] = "{$answer->getArea()}: {$answer->getCoords()} ({$value})";
921 return $parsed_solution;
924 public function getCorrectSolutionForTextOutput(int $active_id, int $pass): array
927 fn(ASS_AnswerImagemap $v): string => "{$v->getArea()}: {$v->getCoords()}"
928 . "({$this->lng->txt('points
')} "
929 . "{$this->lng->txt('checked')}: {$v->getPoints()}, "
930 . "{$this->lng->txt('unchecked')}: {$v->getPointsUnchecked()})",
936 public function toNormalized(
937 Transformations $transformations,
939 ): array|float|bool|int|string|null
942 ...$transformations->normalize(parent::toNormalized($transformations, $context)),
943 'image
' => $transformations->normalize(new QuestionImage($this->image_filename, $this->getId())),
944 'multiple_choice
' => $this->is_multiple_choice,
945 'answers
' => $transformations->normalize($this->answers),
950 public function fromNormalized(
952 Transformations $transformations
955 $clone = parent::fromNormalized($normalized, $transformations);
956 $clone->image_filename = $transformations->denormalize($normalized['image
'], QuestionImage::class)->getFilename();
957 $clone->is_multiple_choice = $transformations->bool($normalized['multiple_choice
']);
958 $clone->answers = array_map(
959 static fn(array $answer): ASS_AnswerImagemap => $transformations->denormalize($answer, new ASS_AnswerImagemap()),
960 $normalized['answers
']
Class for true/false or yes/no answers.
Wrapper object for an image file within a question (e.g.
Class for image map questions.
setIsMultipleChoice($is_multiple_choice)
Set true if the Imagemapquestion is a multiplechoice Question.
uploadImagemap(array $shapes)
loadFromDb(int $question_id)
string $imagemap_contents
__construct( $title='', $comment='', $author='', $owner=-1, $question='', $image_filename='')
assImagemapQuestion constructor
RequestDataCollector $request
copyImagemapFiles(int $source_question_id, int $source_parent_id, int $target_question_id, int $target_parent_id)
uploadImageFile(string $upload_file_name, string $upload_tmp_name)
saveAdditionalQuestionDataToDb()
saveToDb(?int $original_id=null)
getAdditionalTableName()
Returns the name of the additional question data table in the database.
cloneQuestionTypeSpecificProperties(\assQuestion $target)
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
get_imagemap_contents(string $href='#')
getIsMultipleChoice()
Returns true, if the imagemap question is a multiplechoice question.
const MODE_MULTIPLE_CHOICE
setImageFilename(string $image_filename)
saveQuestionDataToDb(?int $original_id=null)
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
static moveUploadedFile(string $a_file, string $a_name, string $a_target, bool $a_raise_errors=true, string $a_mode="move_uploaded")
move uploaded file
static _replaceMediaObjectImageSrc(string $a_text, int $a_direction=0, string $nic='')
Replaces image source from mob image urls with the mob id or replaces mob id with the correct image s...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc