Source: observer.js

  1. /*
  2. * Copyright (c) 2016, Jarmo Juujärvi, Sami Kallio, Kai Korhonen, Juha Moisio, Ilari Paananen
  3. * All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. *
  8. * 1. Redistributions of source code must retain the above copyright
  9. * notice, this list of conditions and the following disclaimer.
  10. *
  11. * 2. Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. *
  15. * 3. Neither the name of the copyright holder nor the names of its
  16. * contributors may be used to endorse or promote products derived
  17. * from this software without specific prior written permission.
  18. *
  19. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  20. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  21. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  22. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
  23. * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  24. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  25. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  26. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  28. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. */
  30. /**
  31. * @fileOverview JavaScript logic for observation view.
  32. * @module observer
  33. * @author Ilari Paananen <ilari.k.paananen at student.jyu.fi>
  34. */
  35. //
  36. // TODO:
  37. // - Remove/comment out console.log calls in release?
  38. //
  39. // NOTE: Functions in observer/index.xhtml.
  40. var CategoryType = getCategoryTypes();
  41. var msg = getMessages();
  42. /**
  43. * The observation clock that can be paused and resumed.
  44. * The individual categories get their time from the observation clock.
  45. * @constructor
  46. */
  47. function Clock() {
  48. this.total_time = 0;
  49. this.resume_time = 0;
  50. this.running = false;
  51. /**
  52. * Resumes the observation clock.
  53. * @param {number} now The time in milliseconds when the observation clock was resumed.
  54. */
  55. this.resume = function(now) {
  56. if (!this.running) {
  57. this.resume_time = now;
  58. this.running = true;
  59. } else {
  60. console.log("Clock.resume(): Clock is already running!");
  61. }
  62. };
  63. /**
  64. * Pauses the observation clock.
  65. * @param {number} now The time in milliseconds when the observation clock was paused.
  66. */
  67. this.pause = function(now) {
  68. if (this.running) {
  69. var delta_time = now - this.resume_time;
  70. this.total_time += delta_time;
  71. this.running = false;
  72. } else {
  73. console.log("Clock.pause(): Clock is already paused!");
  74. }
  75. };
  76. /**
  77. * Returns the total time the observation clock has been running in milliseconds.
  78. * @param {number} now The time in milliseconds when the elapsed time was wanted.
  79. */
  80. this.getElapsedTime = function(now) {
  81. if (this.running) {
  82. return this.total_time + (now - this.resume_time);
  83. } else {
  84. return this.total_time;
  85. }
  86. };
  87. /**
  88. * Returns true if the observation clock is paused, otherwise false.
  89. */
  90. this.isPaused = function() {
  91. return !this.running;
  92. };
  93. }
  94. /**
  95. * Converts milliseconds to a string representing time.
  96. * The time format is hh:mm:ss if the given time is at
  97. * least one hour and mm:ss otherwise.
  98. * @param {number} ms The time in milliseconds.
  99. */
  100. function timeToString(ms) {
  101. var t = Math.floor(ms / 1000);
  102. var s = t % 60;
  103. var m = Math.floor(t / 60) % 60;
  104. var h = Math.floor(t / 60 / 60) % 60;
  105. var str = (m < 10 ? "0" + m : m) + ":" + (s < 10 ? "0" + s : s);
  106. if (h > 0) {
  107. str = (h < 10 ? "0" + h : h) + ":" + str;
  108. }
  109. return str;
  110. }
  111. /**
  112. * Returns the given count as a string with abbreviation, e.g. "13 ct.".
  113. * @param {number} count The count to make the string from.
  114. */
  115. function countToString(count) {
  116. return count + " " + msg.countAbbreviation;
  117. }
  118. /**
  119. * The class acts as a category button. It creates the HTML
  120. * elements it needs and responds to the click events it gets.
  121. * @constructor
  122. * @param {String} name The name to be displayed on the button.
  123. * @param {number} type The type of the category (TIMED or COUNTED).
  124. * @param {number} index The index of the category button.
  125. * @returns {CategoryItem}
  126. */
  127. function CategoryItem(name, type, id, index) {
  128. this.li = $(document.createElement("li"));
  129. this.li.addClass("category-item");
  130. this.li.attr("id", "category-item_" + index);
  131. this.value_div = $(document.createElement("div"));
  132. this.value_div.addClass("category-value");
  133. this.name_div = $(document.createElement("div"));
  134. this.name_div.addClass("category-name");
  135. this.name_div.append(document.createTextNode(name));
  136. this.li.append(this.value_div);
  137. this.li.append(this.name_div);
  138. this.type = type;
  139. this.id = id;
  140. // Used if type is COUNTED.
  141. this.count = 0;
  142. // Used if type is TIMED.
  143. this.time = 0;
  144. this.start_time = 0;
  145. this.down = false;
  146. if (this.type === CategoryType.TIMED)
  147. initTimedCategory(this);
  148. else
  149. initCountedCategory(this);
  150. /**
  151. * The private method replaces the contents of the HTML element
  152. * that displays the value of the category button.
  153. * @param {CategoryItem} this_ The category button.
  154. * @param {String} text The text to replace the contents of the element with.
  155. */
  156. function updateValueDiv(this_, text) {
  157. this_.value_div.empty();
  158. this_.value_div.append(document.createTextNode(text));
  159. }
  160. /**
  161. * The private method initializes the category button to behave
  162. * as a time interval category.
  163. * @param {CategoryItem} this_ The category button.
  164. */
  165. function initTimedCategory(this_) {
  166. updateValueDiv(this_, timeToString(0));
  167. /*
  168. * Click handler for timed category item.
  169. */
  170. this_.click = function(master_time) {
  171. var record;
  172. if (this.down) {
  173. this.li.removeClass("down");
  174. if (master_time > this.start_time) {
  175. this.time += master_time - this.start_time;
  176. record = {id: this.id, startTime: this.start_time, endTime: master_time};
  177. }
  178. this.down = false;
  179. } else {
  180. this.li.addClass("down");
  181. this.start_time = master_time;
  182. this.down = true;
  183. }
  184. return record;
  185. };
  186. /*
  187. * Updates category item's timer div if the category type is timed.
  188. */
  189. this_.updateTimer = function(master_time) {
  190. var time = this.time;
  191. if (this.down) {
  192. time += master_time - this.start_time;
  193. }
  194. updateValueDiv(this, timeToString(time));
  195. };
  196. }
  197. /**
  198. * The private method initializes the category button to behave
  199. * as a category that counts the click events it gets.
  200. * @param {CategoryItem} this_ The category button.
  201. */
  202. function initCountedCategory(this_) {
  203. updateValueDiv(this_, countToString(0));
  204. /*
  205. * Click handler for counted category item.
  206. */
  207. this_.click = function(master_time, paused) {
  208. if (paused) return;
  209. this.count += 1;
  210. updateValueDiv(this, countToString(this.count));
  211. this.li.addClass("down");
  212. var item = this.li;
  213. setTimeout(function() { item.removeClass("down"); }, 50);
  214. return {id: this.id, startTime: master_time, endTime: master_time};
  215. };
  216. /*
  217. * Does nothing because the category type is counted.
  218. */
  219. this_.updateTimer = function() { };
  220. }
  221. }
  222. /**
  223. * The class does the actual observation. It keeps the records
  224. * made during the observation and sends them to the backend after
  225. * the observation is stopped.
  226. * @constructor
  227. * @param category_sets The array of the category sets to be used in the observation.
  228. * @returns {Observer} Constructed observer.
  229. */
  230. function Observer(category_sets) {
  231. this.master_clock = new Clock();
  232. this.categories = [];
  233. this.records = [];
  234. this.started = false;
  235. this.waiting = false;
  236. initialize(this);
  237. /**
  238. * The private method initializes the observer. It creates
  239. * the category buttons and adds them to the HTML element tree.
  240. * @param {Observer} this_ The observer to be initialized.
  241. */
  242. function initialize(this_) {
  243. $("#continue").hide();
  244. $("#pause").hide();
  245. $("#stop").hide();
  246. $("#total-time").append(document.createTextNode(timeToString(0)));
  247. var category_list = $("#category-list");
  248. var index = 0;
  249. for (var i = 0; i < category_sets.length; i++) {
  250. var set = category_sets[i];
  251. if (set.categories.length > 0) {
  252. var category_set = $(document.createElement("ul"));
  253. category_set.attr("id", set.name);
  254. category_set.addClass("category-set");
  255. for (var j = 0; j < set.categories.length; j++) {
  256. var cat = set.categories[j];
  257. var category = new CategoryItem(cat.name, cat.type, cat.id, index);
  258. this_.categories.push(category);
  259. category_set.append(category.li);
  260. index += 1;
  261. }
  262. category_list.append(category_set);
  263. }
  264. }
  265. $(".category-item").addClass("disabled");
  266. }
  267. /**
  268. * The private method adds the record to the records list if it's not undefined.
  269. * The method is used by categoryClick() and stopClick().
  270. * @param record The record or undefined if there is nothing to be added.
  271. */
  272. function addRecord(this_, record) {
  273. if (record !== undefined) {
  274. this_.records.push(record);
  275. }
  276. }
  277. /**
  278. * The event handler starts the observation. It sends an AJAX notification
  279. * to the backend when the observation is started.
  280. */
  281. this.startClick = function() {
  282. if (this.waiting) return;
  283. this.waiting = true;
  284. var this_ = this;
  285. $.ajax({
  286. url: "../../webapi/records/startobservation",
  287. type: "POST",
  288. dataType: "text",
  289. contentType: "text/plain",
  290. cache: false,
  291. data: "start observation",
  292. success: function(data) {
  293. this_.master_clock.resume(Date.now());
  294. this_.started = true;
  295. this_.waiting = false;
  296. var start_button = $("#start");
  297. start_button.off("click");
  298. start_button.hide();
  299. $("#pause").show();
  300. $("#stop-disabled").hide();
  301. $("#stop").show();
  302. $(".category-item").removeClass("disabled");
  303. },
  304. error: function(xhr, status, error) {
  305. showError(msg.obs_errorCouldntSendStart + " " + error);
  306. this_.waiting = false;
  307. }
  308. });
  309. };
  310. /**
  311. * The event handler continues the observation.
  312. */
  313. this.continueClick = function () {
  314. if (this.master_clock.isPaused()) {
  315. this.master_clock.resume(Date.now());
  316. $("#continue").hide();
  317. $("#pause").show();
  318. }
  319. };
  320. /**
  321. * The event handler pauses the observation.
  322. */
  323. this.pauseClick = function() {
  324. if (!this.master_clock.isPaused()) {
  325. this.master_clock.pause(Date.now());
  326. $("#pause").hide();
  327. $("#continue").show();
  328. }
  329. };
  330. /**
  331. * The event handler stops the observation.
  332. * It disables the continue, pause and category buttons.
  333. * If some categories were still on, it stops them
  334. * and creates records accordingly. It sends the recorded
  335. * information to the backend with AJAX and
  336. * redirects the user to the summary page (on success).
  337. */
  338. this.stopClick = function() {
  339. if (!this.started || this.waiting) return;
  340. this.waiting = true;
  341. var now = Date.now();
  342. if (!this.master_clock.isPaused()) {
  343. this.master_clock.pause(now);
  344. }
  345. var continue_button = $("#continue");
  346. var pause_button = $("#pause");
  347. continue_button.off("click");
  348. continue_button.addClass("disabled");
  349. pause_button.off("click");
  350. pause_button.addClass("disabled");
  351. var time = this.master_clock.getElapsedTime(now);
  352. for (var i = 0; i < this.categories.length; i++) {
  353. var category = this.categories[i];
  354. category.li.off("click");
  355. category.li.addClass("disabled");
  356. if (category.down) {
  357. addRecord(this, category.click(time));
  358. }
  359. }
  360. var this_ = this;
  361. $.ajax({
  362. url: "../../webapi/records/addobservationdata",
  363. type: "POST",
  364. dataType: "text",
  365. contentType: "application/json",
  366. cache: false,
  367. data: JSON.stringify({
  368. duration: time,
  369. timeZoneOffsetInMs: getTimeZoneOffset(),
  370. daylightSavingInMs: getDaylightSaving(),
  371. data: this.records
  372. }),
  373. success: function(data) {
  374. this_.waiting = false;
  375. // TODO: Redirect properly.
  376. window.location = "../summary/";
  377. },
  378. error: function(xhr, status, error) {
  379. showError(msg.obs_errorCouldntSendData + ": " + error);
  380. this_.waiting = false;
  381. }
  382. });
  383. };
  384. /**
  385. * Delegates the click of a category button to the correct category.
  386. * It adds the (possible) record returned by the category to the
  387. * list of all the records made during the observation.
  388. * @param {number} index The index of the category button that was clicked.
  389. */
  390. this.categoryClick = function(index) {
  391. var category = this.categories[index];
  392. var time = this.master_clock.getElapsedTime(Date.now());
  393. addRecord(this, category.click(time, this.master_clock.isPaused()));
  394. };
  395. /**
  396. * Updates the observation clock and all the categories based on it.
  397. */
  398. this.tick = function() {
  399. var time = this.master_clock.getElapsedTime(Date.now());
  400. var time_str = timeToString(time);
  401. var total_time = $("#total-time");
  402. total_time.empty();
  403. total_time.append(document.createTextNode(time_str));
  404. for (var i = 0; i < this.categories.length; i++) {
  405. this.categories[i].updateTimer(time);
  406. }
  407. };
  408. }
  409. /**
  410. * Sends an AJAX keep-alive signal to the backend.
  411. */
  412. function keepAlive() {
  413. $.ajax({
  414. url: "../../webapi/records/keepalive",
  415. type: "POST",
  416. dataType: "text",
  417. contentType: "text/plain",
  418. cache: false,
  419. data: "keep-alive",
  420. success: function(data) {
  421. //console.log("Success: " + data);
  422. },
  423. error: function(xhr, status, error) {
  424. showError(msg.obs_errorKeepAliveFailed + ": " + error);
  425. }
  426. });
  427. }
  428. /**
  429. * Shows an error message in a PrimeFaces growl.
  430. * @param {String} error_msg The error message to be shown.
  431. */
  432. function showError(error_msg) {
  433. var growl = PF("growlWdgt");
  434. growl.removeAll();
  435. growl.renderMessage({
  436. summary: msg.dialogErrorTitle,
  437. detail: error_msg,
  438. severity: "error"
  439. });
  440. }
  441. /**
  442. * The function will call observer.stop().
  443. * It is needed if the stopping of the observation has to be confirmed.
  444. */
  445. var stopObservation = function () {};
  446. /**
  447. * This function is ran when the document is ready.
  448. * Creates observer, binds event handlers, and sets two intervals:
  449. * one that updates the observer and one that sends keep alive to backend.
  450. */
  451. $(document).ready(function() {
  452. var category_sets = getCategorySets(); // NOTE: Function in observer/index.xhtml.
  453. var observer = new Observer(category_sets);
  454. $("#start").click(function() {
  455. observer.startClick();
  456. $(".category-item").click(function() {
  457. var id = $(this).attr("id");
  458. var index = parseInt(id.split("_")[1]);
  459. observer.categoryClick(index);
  460. });
  461. });
  462. $("#continue").click(function() { observer.continueClick(); });
  463. $("#pause").click(function() { observer.pauseClick(); });
  464. stopObservation = function() { observer.stopClick(); };
  465. setInterval(function() { observer.tick(); }, 200);
  466. setInterval(keepAlive, 5*60000); // Send keep-alive every 5 minutes.
  467. });
  468. /**
  469. * Gets the offset of the time zone in milliseconds (in JAVA format).
  470. */
  471. function getTimeZoneOffset(){
  472. return -1 * 60 * 1000 * new Date().getTimezoneOffset();
  473. }
  474. /**
  475. * Gets the daylight saving time offset in milliseconds.
  476. */
  477. function getDaylightSaving() {
  478. var now = new Date();
  479. var jan = new Date(now.getFullYear(), 0, 1);
  480. return (jan.getTimezoneOffset() - now.getTimezoneOffset()) * 60 * 1000;
  481. }