From a1c4c6fe62c53f39717d485cb7d4b816f3f62355 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos Date: Fri, 24 Mar 2017 17:28:50 -0500 Subject: [PATCH] Adding Annotation question type - refs BT#10892 Work in progress --- main/exercise/Annotation.php | 103 ++++++ main/exercise/annotation_user.php | 65 ++++ main/exercise/exercise.class.php | 79 ++++- main/exercise/exercise_report.php | 2 + main/exercise/exercise_show.php | 41 ++- main/exercise/exercise_submit.php | 1 + main/exercise/question.class.php | 3 +- main/inc/lib/api.lib.php | 4 +- main/inc/lib/exercise.lib.php | 67 +++- main/inc/lib/exercise_show_functions.lib.php | 18 ++ .../javascript/annotation/js/annotation.js | 299 ++++++++++++++++++ 11 files changed, 672 insertions(+), 10 deletions(-) create mode 100644 main/exercise/Annotation.php create mode 100644 main/exercise/annotation_user.php create mode 100644 main/inc/lib/javascript/annotation/js/annotation.js diff --git a/main/exercise/Annotation.php b/main/exercise/Annotation.php new file mode 100644 index 0000000000..0e3c92f23a --- /dev/null +++ b/main/exercise/Annotation.php @@ -0,0 +1,103 @@ + + * @package chamilo. + */ +class Annotation extends Question +{ + public static $typePicture = 'annotation.png'; + public static $explanationLangVar = 'Annotation'; + + /** + * Annotation constructor. + */ + public function __construct() + { + parent::__construct(); + $this->type = ANNOTATION; + } + + public function display() + { + } + + /** + * @param FormValidator $form + * @param int $fck_config + */ + public function createForm(&$form, $fck_config = 0) + { + parent::createForm($form, $fck_config); + + if (isset($_GET['editQuestion'])) { + $form->addButtonUpdate(get_lang('ModifyExercise'), 'submitQuestion'); + + return; + } + + $form->addElement( + 'file', + 'imageUpload', + array( + Display::img( + Display::return_icon('annotation.png', null, null, ICON_SIZE_BIG, false, true) + ), + get_lang('UploadJpgPicture'), + ) + ); + + $form->addButtonSave(get_lang('GoToQuestion'), 'submitQuestion'); + $form->addRule('imageUpload', get_lang('OnlyImagesAllowed'), 'filetype', array('jpg', 'jpeg', 'png', 'gif')); + $form->addRule('imageUpload', get_lang('NoImage'), 'uploadedfile'); + } + + /** + * @param FormValidator $form + * @param null $objExercise + * @return null|false + */ + public function processCreation($form, $objExercise = null) + { + $fileInfo = $form->getSubmitValue('imageUpload'); + $courseInfo = api_get_course_info(); + + parent::processCreation($form, $objExercise); + + if (!empty($fileInfo['tmp_name'])) { + $this->uploadPicture($fileInfo['tmp_name'], $fileInfo['name']); + + $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document'; + $picturePath = $documentPath.'/images'; + + // fixed width ang height + if (!file_exists($picturePath.'/'.$this->picture)) { + return false; + } + + $this->resizePicture('width', 800); + $this->save(); + + return true; + } + } + + /** + * @param FormValidator $form + */ + function createAnswersForm($form) + { + // nothing + } + + /** + * @param FormValidator $form + */ + function processAnswersCreation($form) + { + // nothing + } +} diff --git a/main/exercise/annotation_user.php b/main/exercise/annotation_user.php new file mode 100644 index 0000000000..7e05144e30 --- /dev/null +++ b/main/exercise/annotation_user.php @@ -0,0 +1,65 @@ +selectPicture(); +$pictureSize = getimagesize($picturePath.'/'.$objQuestion->selectPicture()); +$pictureWidth = $pictureSize[0]; +$pictureHeight = $pictureSize[1]; + +$data = [ + 'use' => 'user', + 'image' => [ + 'path' => $objQuestion->selectPicturePath(), + 'width' => $pictureSize[0], + 'height' => $pictureSize[1] + ], + 'answers' => [ + 'paths' => [], + 'texts' => [] + ] +]; + +$attemptList = Event::getAllExerciseEventByExeId($exerciseId); + +if (!empty($attemptList) && isset($attemptList[$questionId])) { + $questionAttempt = $attemptList[$questionId][0]; + + if (!empty($questionAttempt['answer'])) { + $answers = explode('|', $questionAttempt['answer']); + + foreach ($answers as $answer) { + $parts = explode(')(', $answer); + $type = array_shift($parts); + + switch ($type) { + case 'P': + $points = []; + + foreach ($parts as $partPoint) { + $points[] = Geometry::decodePoint($partPoint); + } + + $data['answers']['paths'][] = $points; + break; + case 'T': + break; + } + } + } +} + +header('Content-Type: application/json'); + +echo json_encode($data); diff --git a/main/exercise/exercise.class.php b/main/exercise/exercise.class.php index ad02b271ce..4b5c4335e7 100755 --- a/main/exercise/exercise.class.php +++ b/main/exercise/exercise.class.php @@ -3176,7 +3176,8 @@ class Exercise if ($answerType == FREE_ANSWER || $answerType == ORAL_EXPRESSION || - $answerType == CALCULATED_ANSWER + $answerType == CALCULATED_ANSWER || + $answerType == ANNOTATION ) { $nbrAnswers = 1; } @@ -3221,7 +3222,7 @@ class Exercise $answer_correct_array = array(); $orderedHotspots = []; - if ($answerType == HOT_SPOT) { + if ($answerType == HOT_SPOT || $answerType == ANNOTATION) { $orderedHotspots = $em ->getRepository('ChamiloCoreBundle:TrackEHotspot') ->findBy([ @@ -4156,6 +4157,35 @@ class Exercise $_SESSION['hotspot_coord'][1] = $delineation_cord; $_SESSION['hotspot_dest'][1] = $answer_delineation_destination; break; + case ANNOTATION: + if ($from_database) { + $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT + WHERE + exe_id = $exeId AND + question_id= ".$questionId; + $resq = Database::query($sql); + $data = Database::fetch_array($resq); + + $choice = $data['answer']; + $choice = str_replace('\r\n', '', $choice); + $choice = stripslashes($choice); + + $questionScore = empty($data['marks']) ? 0 : $data['marks']; + $totalScore += $questionScore == -1 ? 0 : $questionScore; + + $arrques = $questionName; + $arrans = $choice; + + break; + } + + $studentChoice = $choice; + + if ($studentChoice) { + $questionScore = 0; + $totalScore += 0; + } + break; } // end switch Answertype if ($show_result) { @@ -4455,6 +4485,15 @@ class Exercise ) ); echo ''; + } else if ($answerType == ANNOTATION) { + ExerciseShowFunctions::displayAnnotationAnswer( + $feedback_type, + $choice, + $exeId, + $questionId, + $questionScore, + $results_disabled + ); } } } else { @@ -4800,6 +4839,16 @@ class Exercise ); echo ''; + break; + case ANNOTATION: + ExerciseShowFunctions::displayAnnotationAnswer( + $feedback_type, + $choice, + $exeId, + $questionId, + $questionScore, + $results_disabled + ); break; } } @@ -5020,11 +5069,13 @@ class Exercise } } + $relPath = api_get_path(WEB_CODE_PATH); + if ($answerType == HOT_SPOT || $answerType == HOT_SPOT_ORDER) { // We made an extra table for the answers if ($show_result) { - $relPath = api_get_path(WEB_CODE_PATH); + // if ($origin != 'learnpath') { echo ''; echo " @@ -5048,6 +5099,26 @@ class Exercise "; // } } + } else if ($answerType == ANNOTATION) { + if ($show_result) { + echo ''; + echo ' + + +

' . get_lang('Annotation') . '

+
+ + + + '; + } } //if ($origin != 'learnpath') { @@ -5167,7 +5238,7 @@ class Exercise $answer = $choice; Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->id); // } elseif ($answerType == HOT_SPOT || $answerType == HOT_SPOT_DELINEATION) { - } elseif ($answerType == HOT_SPOT) { + } elseif ($answerType == HOT_SPOT || $answerType == ANNOTATION) { $answer = []; if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) { Database::delete( diff --git a/main/exercise/exercise_report.php b/main/exercise/exercise_report.php index 80681dde68..21a9d64036 100755 --- a/main/exercise/exercise_report.php +++ b/main/exercise/exercise_report.php @@ -149,7 +149,9 @@ if (isset($_REQUEST['comments']) && foreach ($_POST as $key_index => $key_value) { $my_post_info = explode('_', $key_index); + $post_content_id[] = $my_post_info[1]; + if ($my_post_info[0] == 'comments') { $comments_exist = true; } diff --git a/main/exercise/exercise_show.php b/main/exercise/exercise_show.php index 76aa282622..1ba94db423 100755 --- a/main/exercise/exercise_show.php +++ b/main/exercise/exercise_show.php @@ -145,6 +145,7 @@ $this_section = SECTION_COURSES; $htmlHeadXtra[] = ''; $htmlHeadXtra[] = ''; +$htmlHeadXtra[] = ''; if ($origin != 'learnpath') { Display::display_header(''); @@ -362,6 +363,8 @@ foreach ($questionList as $questionId) { $choice = array(); } + $relPath = api_get_path(WEB_CODE_PATH); + switch ($answerType) { case MULTIPLE_ANSWER_COMBINATION: //no break @@ -431,7 +434,6 @@ foreach ($questionList as $questionId) { $totalScore += $question_result['score']; if ($show_results) { - $relPath = api_get_path(WEB_CODE_PATH); echo ''; echo " @@ -612,6 +614,39 @@ foreach ($questionList as $questionId) { "; } break; + case ANNOTATION: + $question_result = $objExercise->manage_answer( + $id, + $questionId, + $choice, + 'exercise_show', + array(), + false, + true, + $show_results, + $objExercise->selectPropagateNeg(), + [], + $showTotalScoreAndUserChoicesInLastAttempt + ); + $questionScore = $question_result['score']; + $totalScore += $question_result['score']; + + if ($show_results) { + echo ' +
+ + '; + } + break; } if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) { @@ -642,7 +677,7 @@ foreach ($questionList as $questionId) { if ($isFeedbackAllowed) { $name = "fckdiv" . $questionId; $marksname = "marksName" . $questionId; - if (in_array($answerType, array(FREE_ANSWER, ORAL_EXPRESSION))) { + if (in_array($answerType, array(FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION))) { $url_name = get_lang('EditCommentsAndMarks'); } else { if ($action == 'edit') { @@ -704,7 +739,7 @@ foreach ($questionList as $questionId) { } if ($is_allowedToEdit && $isFeedbackAllowed) { - if (in_array($answerType, array(FREE_ANSWER, ORAL_EXPRESSION))) { + if (in_array($answerType, array(FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION))) { $marksname = "marksName" . $questionId; echo ' HOTSPOT; + } elseif ($answerType == ANNOTATION) { + global $exerciseId, $exe_id; + + $relPath = api_get_path(WEB_CODE_PATH); + + if ($freeze) { + echo << + +HTML; + return ''; + } + + if (api_is_platform_admin() || api_is_course_admin()) { + $course = api_get_course_info(); + $docId = DocumentManager::get_document_id($course, '/images/' . $pictureName); + + if (is_numeric($docId)) { + $images_folder_visibility = api_get_item_visibility( + $course, + 'document', + $docId, + api_get_session_id() + ); + + if (!$images_folder_visibility) { + echo Display::return_message(get_lang('ChangeTheVisibilityOfTheCurrentImage'), 'warning'); + } + } + } + + if (!$only_questions) { + if ($show_title) { + TestCategory::displayCategoryAndTitle($objQuestionTmp->id); + echo '
'.$current_item.'. '.$objQuestionTmp->selectTitle().'
'; + } + + echo << +
+ {$objQuestionTmp->selectDescription()} +
+ +
+HTML; + } + + $objAnswerTmp = new Answer($questionId); + $nbrAnswers = $objAnswerTmp->selectNbrAnswers(); + + unset($objAnswerTmp, $objQuestionTmp); } return $nbrAnswers; } diff --git a/main/inc/lib/exercise_show_functions.lib.php b/main/inc/lib/exercise_show_functions.lib.php index 40657ce19e..ea3577c8c3 100755 --- a/main/inc/lib/exercise_show_functions.lib.php +++ b/main/inc/lib/exercise_show_functions.lib.php @@ -569,4 +569,22 @@ class ExerciseShowFunctions '.Display::return_message(get_lang('notCorrectedYet')); + } + } + } } diff --git a/main/inc/lib/javascript/annotation/js/annotation.js b/main/inc/lib/javascript/annotation/js/annotation.js new file mode 100644 index 0000000000..cfdc297786 --- /dev/null +++ b/main/inc/lib/javascript/annotation/js/annotation.js @@ -0,0 +1,299 @@ +/* For licensing terms, see /license.txt */ + +(function (window) { + /** + * @param referenceElement Element to get the point + * @param x MouseEvent's clientX + * @param y MouseEvent's clientY + * @returns {{x: number, y: number}} + */ + function getPointOnImage(referenceElement, x, y) { + var pointerPosition = { + left: x + window.scrollX, + top: y + window.scrollY + }, + canvasOffset = { + x: referenceElement.getBoundingClientRect().left + window.scrollX, + y: referenceElement.getBoundingClientRect().top + window.scrollY + }; + + return { + x: Math.round(pointerPosition.left - canvasOffset.x), + y: Math.round(pointerPosition.top - canvasOffset.y) + }; + }; + + /** + * @param Object attributes + * @constructor + */ + var SvgElementModel = function (attributes) { + this.attributes = attributes; + this.id = 0; + this.name = ''; + + this.changeEvent = null; + }; + SvgElementModel.prototype.set = function (key, value) { + this.attributes[key] = value; + + if (this.changeEvent) { + this.changeEvent(this); + } + }; + SvgElementModel.prototype.get = function (key) { + return this.attributes[key]; + }; + SvgElementModel.prototype.onChange = function (callback) { + this.changeEvent = callback; + }; + SvgElementModel.decode = function () { + return new this; + }; + SvgElementModel.prototype.encode = function () { + return ''; + }; + + /** + * @param Object attributes + * @constructor + */ + var SvgPathModel = function (attributes) { + SvgElementModel.call(this, attributes); + }; + SvgPathModel.prototype = Object.create(SvgElementModel.prototype); + SvgPathModel.prototype.addPoint = function (x, y) { + x = parseInt(x); + y = parseInt(y); + + var points = this.get('points'); + points.push([x, y]); + + this.set('points', points); + }; + SvgPathModel.prototype.encode = function () { + var pairedPoints = []; + + this.get('points').forEach(function (point) { + pairedPoints.push( + point.join(';') + ); + }); + + return 'P)(' + pairedPoints.join(')('); + }; + SvgPathModel.decode = function (pathInfo) { + var points = []; + + $(pathInfo).each(function (i, point) { + points.push([point.x, point.y]); + }); + + return new SvgPathModel({points: points}); + }; + + /** + * @param Object model + * @constructor + */ + var SvgPathView = function (model) { + var self = this; + + this.model = model; + this.model.onChange(function () { + self.render(); + }); + + this.el = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + this.el.setAttribute('fill', 'transparent'); + this.el.setAttribute('stroke', 'red'); + this.el.setAttribute('stroke-width', 3); + }; + SvgPathView.prototype.render = function () { + var d = '', + points = this.model.get('points'); + + $.each( + this.model.get('points'), + function (i, point) { + d += (i === 0) ? 'M' : ' L '; + d += point[0] + ' ' + point[1]; + } + ); + + this.el.setAttribute('d', d); + + return this; + }; + + /** + * @constructor + */ + var PathsCollection = function () { + this.models = []; + this.length = 0; + this.addEvent = null; + }; + PathsCollection.prototype.add = function (pathModel) { + pathModel.id = ++this.length; + + this.models.push(pathModel); + + if (this.addEvent) { + this.addEvent(pathModel); + } + }; + PathsCollection.prototype.get = function (index) { + return this.models[index]; + }; + PathsCollection.prototype.onAdd = function (callback) { + this.addEvent = callback; + }; + + /** + * @param pathsCollection + * @param image + * @param questionId + * @constructor + */ + var AnnotationCanvasView = function (pathsCollection, image, questionId) { + var self = this; + + this.el = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this.$el = $(this.el); + + this.questionId = parseInt(questionId); + + this.image = image; + + this.pathsCollection = pathsCollection; + this.pathsCollection.onAdd(function (pathModel) { + self.renderPath(pathModel); + }); + }; + AnnotationCanvasView.prototype.render = function () { + this.el.setAttribute('version', '1.1'); + this.el.setAttribute('viewBox', '0 0 ' + this.image.width + ' ' + this.image.height); + + var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image'); + svgImage.setAttributeNS('http://www.w3.org/1999/xlink', 'href', this.image.src); + svgImage.setAttribute('width', this.image.width); + svgImage.setAttribute('height', this.image.height); + + this.el.appendChild(svgImage); + this.setEvents(); + + return this; + }; + AnnotationCanvasView.prototype.setEvents = function () { + var self = this; + + var isMoving = false, + pathModel = null; + + self.$el + .on('dragstart', function (e) { + e.preventDefault(); + }) + .on('mousedown', function (e) { + e.preventDefault(); + + var point = getPointOnImage(self.el, e.clientX, e.clientY); + + pathModel = new SvgPathModel({points: [[point.x, point.y]]}); + + self.pathsCollection.add(pathModel); + + isMoving = true; + }) + .on('mousemove', function (e) { + e.preventDefault(); + + if (!isMoving) { + return; + } + + var point = getPointOnImage(self.el, e.clientX, e.clientY); + + if (!pathModel) { + return; + } + + pathModel.addPoint(point.x, point.y); + }) + .on('mouseup', function (e) { + e.preventDefault(); + + if (!isMoving) { + return; + } + + $('input[name="choice[' + self.questionId + '][' + pathModel.id + ']"]').val(pathModel.encode()); + $('input[name="hotspot[' + self.questionId + '][' + pathModel.id + ']"]').val(pathModel.encode()); + + pathModel = null; + + isMoving = false; + }); + }; + AnnotationCanvasView.prototype.renderPath = function (pathModel) { + var pathView = new SvgPathView(pathModel); + + this.el.appendChild(pathView.render().el); + + $('') + .attr({ + type: 'hidden', + name: 'choice[' + this.questionId + '][' + pathModel.id + ']' + }) + .val(pathModel.encode()) + .appendTo(this.el.parentNode); + + $('') + .attr({ + type: 'hidden', + name: 'hotspot[' + this.questionId + '][' + pathModel.id + ']' + }) + .val(pathModel.encode()) + .appendTo(this.el.parentNode); + }; + + window.AnnotationQuestion = function (userSettings) { + var settings = $.extend({ + questionId: 0, + exerciseId: 0, + relPath: '/', + use: 'user' + }, userSettings); + + var xhrUrl = (settings.use == 'preview') + ? 'exercise/annotation_preview.php' + : (settings.use == 'admin') + ? 'exercise/annotation_admin.php' + : 'exercise/annotation_user.php'; + + $ + .getJSON(settings.relPath + xhrUrl, { + question_id: parseInt(settings.questionId), + exe_id: parseInt(settings.exerciseId) + }) + .done(function (questionInfo) { + var image = new Image(); + image.onload = function () { + var pathsCollection = new PathsCollection(), + canvas = new AnnotationCanvasView(pathsCollection, this, settings.questionId); + + $('#annotation-canvas-' + settings.questionId) + .css({width: this.width}) + .html(canvas.render().el); + + $.each(questionInfo.answers.paths, function (i, pathInfo) { + var pathModel = SvgPathModel.decode(pathInfo); + + pathsCollection.add(pathModel); + }); + }; + image.src = questionInfo.image.path; + }); + }; +})(window);