annotation.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. /* For licensing terms, see /license.txt */
  2. (function (window, $) {
  3. "use strict";
  4. function getPointOnImage(referenceElement, x, y) {
  5. var pointerPosition = {
  6. left: x + window.scrollX,
  7. top: y + window.scrollY
  8. },
  9. canvasOffset = {
  10. x: referenceElement.getBoundingClientRect().left + window.scrollX,
  11. y: referenceElement.getBoundingClientRect().top + window.scrollY
  12. };
  13. return {
  14. x: Math.round(pointerPosition.left - canvasOffset.x),
  15. y: Math.round(pointerPosition.top - canvasOffset.y)
  16. };
  17. }
  18. var SvgElementModel = function (attributes) {
  19. this.attributes = attributes;
  20. this.id = 0;
  21. this.name = "";
  22. this.changeEvent = null;
  23. };
  24. SvgElementModel.prototype.set = function (key, value) {
  25. this.attributes[key] = value;
  26. if (this.changeEvent) {
  27. this.changeEvent(this);
  28. }
  29. };
  30. SvgElementModel.prototype.get = function (key) {
  31. return this.attributes[key];
  32. };
  33. SvgElementModel.prototype.onChange = function (callback) {
  34. this.changeEvent = callback;
  35. };
  36. SvgElementModel.decode = function () {
  37. return new this();
  38. };
  39. SvgElementModel.prototype.encode = function () {
  40. return "";
  41. };
  42. var SvgPathModel = function (attributes) {
  43. SvgElementModel.call(this, attributes);
  44. };
  45. SvgPathModel.prototype = Object.create(SvgElementModel.prototype);
  46. SvgPathModel.prototype.addPoint = function (x, y) {
  47. x = parseInt(x);
  48. y = parseInt(y);
  49. var points = this.get("points");
  50. points.push([x, y]);
  51. this.set("points", points);
  52. };
  53. SvgPathModel.prototype.encode = function () {
  54. var pairedPoints = [];
  55. this.get("points").forEach(function (point) {
  56. pairedPoints.push(
  57. point.join(";")
  58. );
  59. });
  60. return "P)(" + pairedPoints.join(")(");
  61. };
  62. SvgPathModel.decode = function (pathInfo) {
  63. var points = [];
  64. $(pathInfo).each(function (i, point) {
  65. points.push([point.x, point.y]);
  66. });
  67. return new SvgPathModel({points: points});
  68. };
  69. var TextModel = function (userAttributes) {
  70. var attributes = $.extend({
  71. text: "",
  72. x: 0,
  73. y: 0,
  74. color: "red",
  75. fontSize: 20
  76. }, userAttributes);
  77. SvgElementModel.call(this, attributes);
  78. };
  79. TextModel.prototype = Object.create(SvgElementModel.prototype);
  80. TextModel.prototype.encode = function () {
  81. return "T)(" + this.get("text") + ")(" + this.get("x") + ";" + this.get("y");
  82. };
  83. TextModel.decode = function (textInfo) {
  84. return new TextModel({
  85. text: textInfo.text,
  86. x: textInfo.x,
  87. y: textInfo.y
  88. });
  89. };
  90. var SvgPathView = function (model) {
  91. var self = this;
  92. this.model = model;
  93. this.model.onChange(function () {
  94. self.render();
  95. });
  96. this.el = document.createElementNS("http://www.w3.org/2000/svg", "path");
  97. this.el.setAttribute("fill", "transparent");
  98. this.el.setAttribute("stroke", "red");
  99. this.el.setAttribute("stroke-width", "3");
  100. };
  101. SvgPathView.prototype.render = function () {
  102. var d = "";
  103. $.each(
  104. this.model.get("points"),
  105. function (i, point) {
  106. d += (i === 0) ? "M" : " L ";
  107. d += point[0] + " " + point[1];
  108. }
  109. );
  110. this.el.setAttribute("d", d);
  111. return this;
  112. };
  113. var TextView = function (model) {
  114. var self = this;
  115. this.model = model;
  116. this.model.onChange(function () {
  117. self.render();
  118. });
  119. this.el = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  120. this.el.setAttribute('fill', this.model.get('color'));
  121. this.el.setAttribute('font-size', this.model.get('fontSize'));
  122. this.el.setAttribute('stroke', 'none');
  123. };
  124. TextView.prototype.render = function () {
  125. this.el.setAttribute('x', this.model.get('x'));
  126. this.el.setAttribute('y', this.model.get('y'));
  127. this.el.textContent = this.model.get('text');
  128. return this;
  129. };
  130. var ElementsCollection = function () {
  131. this.models = [];
  132. this.length = 0;
  133. this.addEvent = null;
  134. };
  135. ElementsCollection.prototype.add = function (pathModel) {
  136. pathModel.id = ++this.length;
  137. this.models.push(pathModel);
  138. if (this.addEvent) {
  139. this.addEvent(pathModel);
  140. }
  141. };
  142. ElementsCollection.prototype.get = function (index) {
  143. return this.models[index];
  144. };
  145. ElementsCollection.prototype.onAdd = function (callback) {
  146. this.addEvent = callback;
  147. };
  148. var AnnotationCanvasView = function (elementsCollection, image, questionId) {
  149. var self = this;
  150. this.questionId = parseInt(questionId);
  151. this.image = image;
  152. this.el = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  153. this.el.setAttribute('version', '1.1');
  154. this.el.setAttribute('viewBox', '0 0 ' + this.image.width + ' ' + this.image.height);
  155. this.el.setAttribute('width', this.image.width);
  156. this.el.setAttribute('height', this.image.height);
  157. var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image');
  158. svgImage.setAttributeNS('http://www.w3.org/1999/xlink', 'href', this.image.src);
  159. svgImage.setAttribute('width', this.image.width);
  160. svgImage.setAttribute('height', this.image.height);
  161. this.el.appendChild(svgImage);
  162. this.$el = $(this.el);
  163. this.elementsCollection = elementsCollection;
  164. this.elementsCollection.onAdd(function (pathModel) {
  165. self.renderElement(pathModel);
  166. });
  167. this.$rdbOptions = null;
  168. };
  169. AnnotationCanvasView.prototype.render = function () {
  170. this.setEvents();
  171. this.$rdbOptions = $('[name="' + this.questionId + '-options"]');
  172. return this;
  173. };
  174. AnnotationCanvasView.prototype.setEvents = function () {
  175. var self = this;
  176. var isMoving = false,
  177. elementModel = null;
  178. self.$el
  179. .on('dragstart', function (e) {
  180. e.preventDefault();
  181. })
  182. .on('click', function (e) {
  183. e.preventDefault();
  184. if ("1" !== self.$rdbOptions.filter(':checked').val()) {
  185. return;
  186. }
  187. var point = getPointOnImage(self.el, e.clientX, e.clientY);
  188. elementModel = new TextModel({x: point.x, y: point.y, text: ''});
  189. self.elementsCollection.add(elementModel);
  190. elementModel = null;
  191. isMoving = false;
  192. })
  193. .on('mousedown', function (e) {
  194. e.preventDefault();
  195. var point = getPointOnImage(self.el, e.clientX, e.clientY);
  196. if (isMoving || "0" !== self.$rdbOptions.filter(':checked').val() || elementModel) {
  197. return;
  198. }
  199. elementModel = new SvgPathModel({points: [[point.x, point.y]]});
  200. self.elementsCollection.add(elementModel);
  201. isMoving = true;
  202. })
  203. .on('mousemove', function (e) {
  204. e.preventDefault();
  205. if (!isMoving || "0" !== self.$rdbOptions.filter(':checked').val() || !elementModel) {
  206. return;
  207. }
  208. var point = getPointOnImage(self.el, e.clientX, e.clientY);
  209. elementModel.addPoint(point.x, point.y);
  210. })
  211. .on('mouseup', function (e) {
  212. e.preventDefault();
  213. if (!isMoving || "0" !== self.$rdbOptions.filter(':checked').val() || !elementModel) {
  214. return;
  215. }
  216. elementModel = null;
  217. isMoving = false;
  218. });
  219. };
  220. AnnotationCanvasView.prototype.renderElement = function (elementModel) {
  221. var elementView = null,
  222. self = this;
  223. if (elementModel instanceof SvgPathModel) {
  224. elementView = new SvgPathView(elementModel);
  225. } else if (elementModel instanceof TextModel) {
  226. elementView = new TextView(elementModel);
  227. }
  228. if (!elementView) {
  229. return;
  230. }
  231. $('<input>')
  232. .attr({
  233. type: 'hidden',
  234. name: 'choice[' + this.questionId + '][' + elementModel.id + ']'
  235. })
  236. .val(elementModel.encode())
  237. .appendTo(this.el.parentNode);
  238. $('<input>')
  239. .attr({
  240. type: 'hidden',
  241. name: 'hotspot[' + this.questionId + '][' + elementModel.id + ']'
  242. })
  243. .val(elementModel.encode())
  244. .appendTo(this.el.parentNode);
  245. this.el.appendChild(elementView.render().el);
  246. elementModel.onChange(function () {
  247. elementView.render();
  248. $('input[name="choice[' + self.questionId + '][' + elementModel.id + ']"]').val(elementModel.encode());
  249. $('input[name="hotspot[' + self.questionId + '][' + elementModel.id + ']"]').val(elementModel.encode());
  250. });
  251. if (elementModel instanceof TextModel) {
  252. $('<input>')
  253. .attr({
  254. type: 'text',
  255. name: 'text[' + this.questionId + '][' + elementModel.id + ']'
  256. })
  257. .addClass('form-control input-sm')
  258. .on('change', function (e) {
  259. elementModel.set('text', this.value);
  260. e.preventDefault();
  261. })
  262. .val(elementModel.get('text'))
  263. .appendTo('#annotation-toolbar-' + this.questionId + ' ul')
  264. .wrap('<li class="form-group"></li>')
  265. .focus();
  266. }
  267. };
  268. window.AnnotationQuestion = function (userSettings) {
  269. $(function () {
  270. var settings = $.extend(
  271. {
  272. questionId: 0,
  273. exerciseId: 0,
  274. relPath: '/'
  275. },
  276. userSettings
  277. ),
  278. xhrUrl = 'exercise/annotation_user.php?' + _p.web_cid_query,
  279. $container = $('#annotation-canvas-' + settings.questionId);
  280. $.getJSON(settings.relPath + xhrUrl, {
  281. question_id: parseInt(settings.questionId),
  282. exe_id: parseInt(settings.exerciseId),
  283. course_id: parseInt(settings.courseId)
  284. })
  285. .done(function (questionInfo) {
  286. var image = new Image();
  287. image.onload = function () {
  288. var elementsCollection = new ElementsCollection(),
  289. canvas = new AnnotationCanvasView(elementsCollection, this, settings.questionId);
  290. $container.html(canvas.render().el);
  291. /** @namespace questionInfo.answers.paths */
  292. $.each(questionInfo.answers.paths, function (i, pathInfo) {
  293. var pathModel = SvgPathModel.decode(pathInfo);
  294. elementsCollection.add(pathModel);
  295. });
  296. /** @namespace questionInfo.answers.texts */
  297. $(questionInfo.answers.texts).each(function (i, textInfo) {
  298. var textModel = TextModel.decode(textInfo);
  299. elementsCollection.add(textModel);
  300. });
  301. };
  302. image.src = questionInfo.image.path;
  303. });
  304. });
  305. };
  306. })(window, window.jQuery);