Source: summary.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. /* global PF, links, SummaryIndex */
  31. /**
  32. * @fileOverview Javascript methods for the summary page.
  33. * @module summary
  34. * @author Juha Moisio <juha.pa.moisio at student.jyu.fi>
  35. */
  36. var TIMELINE_BEGIN = getLocalZeroDate();
  37. var OBSERVATION_DURATION = SummaryIndex.getObservationDuration(); // function
  38. // in
  39. // summary/index.xhtml
  40. var msg = SummaryIndex.getMessages(); // function in summary/index.xhtml
  41. var ESCAPE_KEY = 27;
  42. var URI;
  43. /**
  44. * On document ready: - Calculate recordings summary details. - Update the
  45. * details on time frame change. - Add zoom button click events for timeline
  46. * zooming. - Show growl message on timeline event selection.
  47. */
  48. $(function() {
  49. var timeline = PF("timelineWdgt").getInstance();
  50. var growl = PF("growlWdgt");
  51. var startTimeWdgt = PF("startTimeWdgt");
  52. var endTimeWdgt = PF("endTimeWdgt");
  53. var timeframe = timeline.getVisibleChartRange();
  54. var startTimePicker = $("#startTime_input");
  55. var endTimePicker = $("#endTime_input");
  56. timeline.options.showCurrentTime = false; // NOTE: setting this did not
  57. // work from Summary Bean.
  58. updateRecordsTable(timeline, timeframe);
  59. // Set time select listeners and restore original dates that get reseted on
  60. // event bind.
  61. var startDate = startTimeWdgt.getDate();
  62. var endDate = endTimeWdgt.getDate();
  63. startTimePicker.timepicker("option", "onSelect", function(startTime) {
  64. var error = updateTimelineTimeframe(timeline, startTime, endTimePicker
  65. .val());
  66. startTimePicker.toggleClass("ui-state-error", error);
  67. if (error && convertStrToMs(startTime) > OBSERVATION_DURATION) {
  68. startTimeWdgt.setDate(endDate);
  69. }
  70. });
  71. startTimePicker
  72. .keyup(function() {
  73. var error = updateTimelineTimeframe(timeline, startTimePicker
  74. .val(), endTimePicker.val());
  75. $(this).toggleClass("ui-state-error", error);
  76. if (error
  77. && convertStrToMs(startTimePicker.val()) > OBSERVATION_DURATION) {
  78. startTimeWdgt.setDate(endDate);
  79. }
  80. });
  81. endTimePicker.timepicker("option", "onSelect", function(endTime) {
  82. var error = updateTimelineTimeframe(timeline, startTimePicker.val(),
  83. endTime);
  84. endTimePicker.toggleClass("ui-state-error", error);
  85. if (error && convertStrToMs(endTime) > OBSERVATION_DURATION) {
  86. endTimeWdgt.setDate(endDate);
  87. }
  88. });
  89. endTimePicker
  90. .keyup(function() {
  91. var error = updateTimelineTimeframe(timeline, startTimePicker
  92. .val(), endTimePicker.val());
  93. $(this).toggleClass("ui-state-error", error);
  94. if (error
  95. && convertStrToMs(endTimePicker.val()) > OBSERVATION_DURATION) {
  96. endTimeWdgt.setDate(endDate);
  97. }
  98. });
  99. startTimeWdgt.setDate(startDate);
  100. endTimeWdgt.setDate(endDate);
  101. links.events.addListener(timeline, "select", function() {
  102. showRecordDetails(timeline, growl);
  103. });
  104. $(document).click(function(e) {
  105. if (!$(e.target).hasClass("timeline-event-content")) {
  106. hideMessages(timeline, growl);
  107. }
  108. });
  109. $(document).keyup(function(e) {
  110. if (e.keyCode === ESCAPE_KEY) {
  111. hideMessages(timeline, growl);
  112. }
  113. });
  114. saveImage();
  115. });
  116. /**
  117. * Updates the records table information according to the given time frame.
  118. *
  119. * @param {object}
  120. * timeline - The timeline component.
  121. * @param {object}
  122. * timeframe - The selected start and end time.
  123. */
  124. function updateRecordsTable(timeline, timeframe) {
  125. var recordsTable = $("#records");
  126. var categories = timeline.getItemsByGroup(timeline.items);
  127. var timeframeDuration = getTimeframeDuration(timeframe);
  128. var recordsTotalCount = getRecordsInTimeframe(timeline.items, timeframe).length;
  129. recordsTable.empty();
  130. var oldCategorySet;
  131. $.each(categories, function(category, categoryRecords) {
  132. var records = getRecordsInTimeframe(categoryRecords, timeframe);
  133. var duration = getDurationOfRecords(records, timeframe);
  134. var newCategorySet = category
  135. .match("<span class=categorySet>(.*)</span>")[1];
  136. var recordRow = createRecordRow({
  137. name : category,
  138. count : records.length,
  139. duration : duration,
  140. addGap : oldCategorySet !== newCategorySet,
  141. countPercent : spanPercentOf(records.length, recordsTotalCount),
  142. durationPercent : spanPercentOf(duration, timeframeDuration)
  143. });
  144. recordsTable.append(recordRow);
  145. oldCategorySet = newCategorySet;
  146. });
  147. var summaryRow = createRecordRow({
  148. name : msg.sum_total,
  149. count : recordsTotalCount,
  150. duration : timeframeDuration,
  151. countPercent : " ",
  152. durationPercent : " "
  153. });
  154. summaryRow.addClass("summary-row");
  155. recordsTable.append(summaryRow);
  156. }
  157. /**
  158. * Creates a HTML element containing the data of a record.
  159. *
  160. * @param {object}
  161. * record - The object contains record data in the form: {name,
  162. * count, countPercentage, duration, durationPercentage}
  163. * @returns {object} - The jquery object containing the record row element.
  164. */
  165. function createRecordRow(record, colcount) {
  166. // TODO: escape XSS; Is it required? Values are from backing bean and are
  167. // already escaped and user cannot change them later.
  168. var row = $('<div class="ui-grid-row">');
  169. var count = $('<div class="ui-grid-col-3">');
  170. var duration = $('<div class="ui-grid-col-3">');
  171. count.append('<span>' + record.count + "</span>");
  172. count.append('<span>' + record.countPercent + "</span>");
  173. duration.append('<span>' + convertMsToUnits(record.duration) + "</span>");
  174. duration.append('<span>' + record.durationPercent + "</span>");
  175. row.append('<div class="ui-grid-col-5">' + record.name + "</div>");
  176. row.append(count);
  177. row.append(duration);
  178. if (record.addGap) {
  179. row.addClass("gapBefore");
  180. }
  181. return row;
  182. }
  183. /**
  184. * Updates the time frame of the timeline to the given start and end times.
  185. *
  186. * @param {object}
  187. * timeline - The timeline component.
  188. * @param {string}
  189. * strStart - The time frame starting time in hh:mm:ss format.
  190. * @param {string}
  191. * strEnd - The time frame ending time in hh:mm:ss format.
  192. * @returns {boolean} - returns true on errors, false if updated successfully.
  193. */
  194. function updateTimelineTimeframe(timeline, strStart, strEnd) {
  195. var msStart = convertStrToMs(strStart);
  196. var msEnd = convertStrToMs(strEnd);
  197. // check the validity of time frame
  198. if (msStart >= msEnd || msStart > OBSERVATION_DURATION
  199. || msEnd > OBSERVATION_DURATION) {
  200. return true;
  201. }
  202. if (msStart) {
  203. timeline.options.min = new Date(TIMELINE_BEGIN.getTime() + msStart);
  204. } else {
  205. timeline.options.min = TIMELINE_BEGIN;
  206. }
  207. if (msEnd) {
  208. timeline.options.max = new Date(TIMELINE_BEGIN.getTime() + msEnd);
  209. } else {
  210. timeline.options.max = new Date(TIMELINE_BEGIN.getTime()
  211. + OBSERVATION_DURATION * 1.1);
  212. }
  213. timeline.setVisibleChartRangeAuto();
  214. updateRecordsTable(timeline, timeline.getVisibleChartRange());
  215. return false;
  216. }
  217. /**
  218. * Shows a PrimeFaces growl message with details of the selected record.
  219. *
  220. * @param {object}
  221. * timeline - The timeline component.
  222. * @param {object}
  223. * growl - The growl component.
  224. */
  225. function showRecordDetails(timeline, growl) {
  226. var selection = timeline.getSelection();
  227. if (selection.length) {
  228. if (selection[0].row !== undefined) {
  229. var record = timeline.getItem(selection[0].row);
  230. growl.removeAll();
  231. growl.renderMessage({
  232. summary : record.group,
  233. detail : getRecordDetails(record),
  234. severity : "info"
  235. });
  236. }
  237. }
  238. }
  239. /**
  240. * Hides all growl messages and removes timeline selection.
  241. *
  242. * @param {object}
  243. * timeline - The timeline component.
  244. * @param {object}
  245. * growl - The growl component.
  246. */
  247. function hideMessages(timeline, growl) {
  248. growl.removeAll();
  249. timeline.setSelection(null);
  250. }
  251. /**
  252. * Gets all the records that are fully or partially in the given time frame.
  253. *
  254. * @param {object}
  255. * records - The object containing the records.
  256. * @param {object}
  257. * timeframe - The selected start and end time.
  258. * @returns {object} - returns a list of matched records.
  259. */
  260. function getRecordsInTimeframe(records, timeframe) {
  261. var recordsIn = [];
  262. $.each(records,
  263. function(i, record) {
  264. if (record.className === "dummyRecord") {
  265. return true;
  266. } else if (record.start >= timeframe.start
  267. && record.start < timeframe.end) {
  268. recordsIn.push(record);
  269. } else if (record.end <= timeframe.end
  270. && record.end > timeframe.start) {
  271. recordsIn.push(record);
  272. } else if (record.start < timeframe.start
  273. && record.end > timeframe.end) {
  274. recordsIn.push(record);
  275. }
  276. });
  277. return recordsIn;
  278. }
  279. /**
  280. * Gets the record details as a string.
  281. *
  282. * @param {object}
  283. * record - The record object from the timeline component.
  284. * @returns {string} - The details as a string value.
  285. */
  286. function getRecordDetails(record) {
  287. var details = "";
  288. var start = toTimelineTime(record.start);
  289. var end = toTimelineTime(record.end);
  290. details += msg.sum_begin + ": " + convertMsToStr(start);
  291. details += "<br/>";
  292. details += msg.sum_end + ": " + convertMsToStr(end);
  293. details += "<br/>";
  294. details += msg.sum_duration + ": " + convertMsToUnits(end - start);
  295. return details;
  296. }
  297. /**
  298. * Get total duration of records of all categories in given time frame.
  299. *
  300. * @param {object}
  301. * records - object containing the records.
  302. * @param {object}
  303. * timeframe - The selected start and end time.
  304. * @returns {number} - duration of the records.
  305. */
  306. function getDurationOfCategories(categories, timeframe) {
  307. var duration = 0;
  308. $.each(categories, function(category, records) {
  309. duration += getDurationOfRecords(records, timeframe);
  310. });
  311. return duration;
  312. }
  313. /**
  314. * Gets the duration of the given time frame.
  315. *
  316. * @param {object}
  317. * timeframe - The selected start and end time.
  318. * @returns {number} - duration of the observation's time frame.
  319. */
  320. function getTimeframeDuration(timeframe) {
  321. var rStartMs = toTimelineTime(timeframe.start);
  322. var rEndMs = toTimelineTime(timeframe.end);
  323. var start = (rStartMs > 0) ? rStartMs : 0;
  324. var end = (rEndMs < OBSERVATION_DURATION) ? rEndMs : OBSERVATION_DURATION;
  325. return end - start;
  326. }
  327. /**
  328. * Gets the total duration of the records in the given time frame.
  329. *
  330. * @param {object}
  331. * records - The object containing the records.
  332. * @returns {number} - The duration of the records.
  333. */
  334. function getDurationOfRecords(records, timeframe) {
  335. var duration = 0;
  336. $.each(records, function() {
  337. var start = this.start;
  338. var end = this.end;
  339. if (this.className === "dummyRecord") {
  340. return true;
  341. }
  342. if (start < timeframe.start) {
  343. start = timeframe.start;
  344. }
  345. if (end > timeframe.end) {
  346. end = timeframe.end;
  347. }
  348. if (end > start) {
  349. duration += end - start;
  350. }
  351. });
  352. return duration;
  353. }
  354. /**
  355. * Converts the time in milliseconds to a string hh:mm:ss.
  356. *
  357. * @param {number}
  358. * ms - The time in milliseconds.
  359. * @returns {string} - The time in string as hh:mm:ss.
  360. */
  361. function convertMsToStr(ms) {
  362. var d = ms;
  363. d = Math.floor(d / 1000);
  364. var s = d % 60;
  365. d = Math.floor(d / 60);
  366. var m = d % 60;
  367. d = Math.floor(d / 60);
  368. var h = d % 60;
  369. return [ h, m, s ].map(leadingZero).join(':');
  370. }
  371. /**
  372. * Converts the time string in the form hh:mm:ss to milliseconds.
  373. *
  374. * @param {string}
  375. * str - The time in a string as hh:mm:ss.
  376. * @returns {number} - The time in milliseconds or NaN for unparseable time
  377. * string.
  378. */
  379. function convertStrToMs(str) {
  380. var time = str.split(/:/);
  381. // insert missing values
  382. for (var i = 3 - time.length; i > 0; i--) {
  383. time.unshift("0");
  384. }
  385. var seconds = 0;
  386. for (var i = 0; i < time.length; i++) {
  387. seconds += parseInt(time[i], 10) * Math.pow(60, 2 - i);
  388. }
  389. return seconds * 1000;
  390. }
  391. /**
  392. * Converts the time in milliseconds to a string with the time units e.g. 1h 2m
  393. * 0s.
  394. *
  395. * @param {number}
  396. * ms - The time in milliseconds.
  397. * @returns {string} - The time in string with units e.g. 1h 2m 0s.
  398. */
  399. function convertMsToUnits(ms) {
  400. var time = convertMsToStr(ms).split(":");
  401. var units = "";
  402. var getTimeUnit = function(i, unit) {
  403. var n = parseInt(time[i], 10);
  404. if (n > 0) {
  405. units += n + unit;
  406. }
  407. };
  408. if (ms <= 0) {
  409. return "0 s";
  410. }
  411. if (ms < 1000) {
  412. return "~1 s";
  413. }
  414. if (time.length === 3) {
  415. getTimeUnit(0, " h");
  416. getTimeUnit(1, " m");
  417. getTimeUnit(2, " s");
  418. } else {
  419. return "0 s";
  420. }
  421. return units.replace(/([hms])(\d)/g, "$1 $2");
  422. }
  423. /**
  424. * Returns the given number as a string and appends a leading zero to it if the
  425. * number is a single digit number.
  426. *
  427. * @param {number}
  428. * n - The given number.
  429. * @returns {string} - number with possible leading zero.
  430. */
  431. function leadingZero(n) {
  432. return (n < 10 ? "0" + n : n.toString());
  433. }
  434. /**
  435. * Calculates the percentage of two values.
  436. *
  437. * @param {number}
  438. * a - The number of share.
  439. * @param {number}
  440. * b - The number of total quantity.
  441. * @returns {number} - percentage ratio.
  442. */
  443. function percentOf(a, b) {
  444. if (a === 0 || b === 0) {
  445. return 0;
  446. }
  447. return Math.round((a / b) * 100);
  448. }
  449. /**
  450. * Gets the percentage of two values as a span element string.
  451. *
  452. * @param {number}
  453. * a - The number of share.
  454. * @param {number}
  455. * b - The number of total quantity.
  456. * @returns {string} - percent as span element string.
  457. */
  458. function spanPercentOf(a, b) {
  459. var percent = percentOf(a, b);
  460. var str = " (" + percent.toString() + "%)";
  461. if (percent < 10) {
  462. str = " " + str;
  463. }
  464. if (percent < 100) {
  465. str = " " + str;
  466. }
  467. return '<span class="percent">' + str + "</span>";
  468. }
  469. /**
  470. * Gets the "zero" date with the time zone offset.
  471. *
  472. * @returns {date} - The zero date with the time zone offset.
  473. */
  474. function getLocalZeroDate() {
  475. var localDate = new Date(0);
  476. var zeroDate = new Date(localDate.getTimezoneOffset() * 60 * 1000);
  477. return zeroDate;
  478. }
  479. /**
  480. * Converts the date object to the timeline component time.
  481. *
  482. * @param {date}
  483. * date - The date object of the time to be converted.
  484. * @returns {number} - The converted time in milliseconds.
  485. */
  486. function toTimelineTime(date) {
  487. return Math.abs(TIMELINE_BEGIN.getTime() - date.getTime());
  488. }
  489. /**
  490. * Encodes HTML markup characters to HTML entities.
  491. *
  492. * @param {string}
  493. * str - The string to be encoded.
  494. * @returns {str} - The encoded string.
  495. */
  496. function encodeHTML(str) {
  497. return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g,
  498. '&gt;').replace(/"/g, '&quot;');
  499. }
  500. /**
  501. * Checks if the user has scrolled to the bottom of the page.
  502. *
  503. * @param {number}
  504. * padding - An extra padding to be checked.
  505. * @return {boolean} - true if at bottom otherwise false.
  506. */
  507. function isBottomOfDocument(padding) {
  508. return $(window).scrollTop() >= $(document).height() - padding
  509. - $(window).height();
  510. }
  511. /**
  512. * Checks what checkboxes user has selected and performs action if specific
  513. * checkbox is selected
  514. */
  515. function checkCheckBoxes() {
  516. var checkBox1 = document.getElementById('saveForm:basic:1');
  517. var checkBox2 = document.getElementById('saveForm:anonymityUserBoxes:1');
  518. if (checkBox2 != null) {
  519. if (checkBox2.checked) {
  520. saveAsImage();
  521. }
  522. }
  523. if (checkBox1 != null) {
  524. if (checkBox1.checked) {
  525. saveAsImage();
  526. }
  527. }
  528. }
  529. /**
  530. * Creates canvas element where the timeline and datatable are inserted
  531. * sends the created image through ajax to Java
  532. */
  533. function saveImage() {
  534. html2canvas(document.getElementById('recordingsPhoto')).then(
  535. function(canvas) {
  536. let URI = canvas.toDataURL();
  537. sendImage(URI);
  538. });
  539. }
  540. /**
  541. * Sends the given string to the servlet that expects a base64-encoded png
  542. * @param URI The base64-encoded png-file
  543. */
  544. function sendImage(URI) {
  545. $.ajax({
  546. url : "../../webapi/summary/image",
  547. type : "POST",
  548. dataType : "text",
  549. contentType : "text/plain",
  550. cache : false,
  551. data : "obsimg,"+URI,
  552. success : function(data) {
  553. },
  554. error : function(xhr, status, error) {
  555. showError(msg.obs_errorCouldntSendData + ": " + error);
  556. this_.waiting = false;
  557. }
  558. });
  559. }
  560. /**
  561. * Saves the canvas as png.
  562. */
  563. function saveAsImage() {
  564. var filename;
  565. var filenameRaw;
  566. try {
  567. filenameRaw = document.getElementById('saveForm:input-name').value;
  568. if (filenameRaw == "") {
  569. return;
  570. }
  571. filename = filenameRaw.replace(/\./g, '-');
  572. } catch (err) {
  573. filename = 'summary.png';
  574. }
  575. var link = document.createElement('a');
  576. if (typeof link.download === 'string') {
  577. link.href = URI;
  578. link.download = filename;
  579. // Firefox requires the link to be in the body
  580. document.body.appendChild(link);
  581. // simulate click
  582. link.click();
  583. document.body.removeChild(link);
  584. } else {
  585. window.open(URI);
  586. }
  587. document.body.removeChild(link);
  588. }