/*
* Copyright (c) 2016, Jarmo Juujärvi, Sami Kallio, Kai Korhonen, Juha Moisio, Ilari Paananen
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* global PF, links, SummaryIndex */
/**
* @fileOverview Javascript methods for the summary page.
* @module summary
* @author Juha Moisio <juha.pa.moisio at student.jyu.fi>
*/
var TIMELINE_BEGIN = getLocalZeroDate();
var OBSERVATION_DURATION = SummaryIndex.getObservationDuration(); // function in summary/index.xhtml
var msg = SummaryIndex.getMessages(); // function in summary/index.xhtml
var ESCAPE_KEY = 27;
/**
* On document ready:
* - Calculate recordings summary details.
* - Update the details on time frame change.
* - Add zoom button click events for timeline zooming.
* - Show growl message on timeline event selection.
*/
$(function () {
var timeline = PF("timelineWdgt").getInstance();
var growl = PF("growlWdgt");
var startTimeWdgt = PF("startTimeWdgt");
var endTimeWdgt = PF("endTimeWdgt");
var timeframe = timeline.getVisibleChartRange();
var startTimePicker = $("#startTime_input");
var endTimePicker = $("#endTime_input");
timeline.options.showCurrentTime = false; // NOTE: setting this did not work from Summary Bean.
updateRecordsTable(timeline, timeframe);
// Set time select listeners and restore original dates that get reseted on event bind.
var startDate = startTimeWdgt.getDate();
var endDate = endTimeWdgt.getDate();
startTimePicker.timepicker("option", "onSelect", function (startTime) {
var error = updateTimelineTimeframe(timeline, startTime, endTimePicker.val());
startTimePicker.toggleClass("ui-state-error", error);
if (error && convertStrToMs(startTime) > OBSERVATION_DURATION) {
startTimeWdgt.setDate(endDate);
}
});
startTimePicker.keyup(function () {
var error = updateTimelineTimeframe(timeline, startTimePicker.val(), endTimePicker.val());
$(this).toggleClass("ui-state-error", error);
if (error && convertStrToMs(startTimePicker.val()) > OBSERVATION_DURATION) {
startTimeWdgt.setDate(endDate);
}
});
endTimePicker.timepicker("option", "onSelect", function (endTime) {
var error = updateTimelineTimeframe(timeline, startTimePicker.val(), endTime);
endTimePicker.toggleClass("ui-state-error", error);
if (error && convertStrToMs(endTime) > OBSERVATION_DURATION) {
endTimeWdgt.setDate(endDate);
}
});
endTimePicker.keyup(function () {
var error = updateTimelineTimeframe(timeline, startTimePicker.val(), endTimePicker.val());
$(this).toggleClass("ui-state-error", error);
if (error && convertStrToMs(endTimePicker.val()) > OBSERVATION_DURATION) {
endTimeWdgt.setDate(endDate);
}
});
startTimeWdgt.setDate(startDate);
endTimeWdgt.setDate(endDate);
links.events.addListener(timeline, "select", function () {
showRecordDetails(timeline, growl);
});
$(document).click(function (e) {
if (!$(e.target).hasClass("timeline-event-content")) {
hideMessages(timeline, growl);
}
});
$(document).keyup(function (e) {
if (e.keyCode === ESCAPE_KEY) {
hideMessages(timeline, growl);
}
});
/* Disabled */
/*
$("#total-records").text(getRecordsInTimeframe(timeline.items, timeframe).length);
$("#total-duration").text(convertMsToUnits(OBSERVATION_DURATION));
$("#button-zoom-in").click(function () {
timeline.zoom(0.2, TIMELINE_BEGIN);
});
$("#button-zoom-out").click(function () {
timeline.zoom(-0.2);
});
$(window).on('scroll resize', function () {
$("#timelineControls").toggleClass("bottom",
isBottomOfDocument($("#Footer").height()));
});
$("#timelineControls").toggleClass("bottom",
isBottomOfDocument($("#Footer").height()));
*/
/* Ask confirmation before leaving unsaved observation data */
/*
window.onbeforeunload = function () {
return msg.dlg_confirmLeave;
};
*/
});
/**
* Updates the records table information according to the given time frame.
* @param {object} timeline - The timeline component.
* @param {object} timeframe - The selected start and end time.
*/
function updateRecordsTable(timeline, timeframe) {
var recordsTable = $("#records");
var categories = timeline.getItemsByGroup(timeline.items);
var timeframeDuration = getTimeframeDuration(timeframe);
var recordsTotalCount = getRecordsInTimeframe(timeline.items, timeframe).length;
recordsTable.empty();
var oldCategorySet;
$.each(categories, function (category, categoryRecords) {
var records = getRecordsInTimeframe(categoryRecords, timeframe);
var duration = getDurationOfRecords(records, timeframe);
var newCategorySet = category.match("<span class=categorySet>(.*)</span>")[1];
var recordRow = createRecordRow({
name: category,
count: records.length,
duration: duration,
addGap: oldCategorySet !== newCategorySet,
countPercent: spanPercentOf(records.length, recordsTotalCount),
durationPercent: spanPercentOf(duration, timeframeDuration)
});
recordsTable.append(recordRow);
oldCategorySet = newCategorySet;
});
var summaryRow = createRecordRow({
name: msg.sum_total,
count: recordsTotalCount,
duration: timeframeDuration,
countPercent: " ",
durationPercent: " "
});
summaryRow.addClass("summary-row");
recordsTable.append(summaryRow);
}
/**
* Creates a HTML element containing the data of a record.
* @param {object} record - The object contains record data
* in the form: {name, count, countPercentage, duration, durationPercentage}
* @returns {object} - The jquery object containing the record row element.
*/
function createRecordRow(record, colcount) {
// TODO: escape XSS; Is it required? Values are from backing bean and are
// already escaped and user cannot change them later.
var row = $('<div class="ui-grid-row">');
var count = $('<div class="ui-grid-col-3">');
var duration = $('<div class="ui-grid-col-3">');
count.append('<span>' + record.count + "</span>");
count.append('<span>' + record.countPercent + "</span>");
duration.append('<span>' + convertMsToUnits(record.duration) + "</span>");
duration.append('<span>' + record.durationPercent + "</span>");
row.append('<div class="ui-grid-col-5">' + record.name + "</div>");
row.append(count);
row.append(duration);
if (record.addGap) {
row.addClass("gapBefore");
}
return row;
}
/**
* Updates the time frame of the timeline to the given start and end times.
* @param {object} timeline - The timeline component.
* @param {string} strStart - The time frame starting time in hh:mm:ss format.
* @param {string} strEnd - The time frame ending time in hh:mm:ss format.
* @returns {boolean} - returns true on errors, false if updated successfully.
*/
function updateTimelineTimeframe(timeline, strStart, strEnd) {
var msStart = convertStrToMs(strStart);
var msEnd = convertStrToMs(strEnd);
// check the validity of time frame
if (msStart >= msEnd
|| msStart > OBSERVATION_DURATION
|| msEnd > OBSERVATION_DURATION) {
return true;
}
if (msStart) {
timeline.options.min = new Date(TIMELINE_BEGIN.getTime() + msStart);
} else {
timeline.options.min = TIMELINE_BEGIN;
}
if (msEnd) {
timeline.options.max = new Date(TIMELINE_BEGIN.getTime() + msEnd);
} else {
timeline.options.max = new Date(TIMELINE_BEGIN.getTime() + OBSERVATION_DURATION * 1.1);
}
timeline.setVisibleChartRangeAuto();
updateRecordsTable(timeline, timeline.getVisibleChartRange());
return false;
}
/**
* Shows a PrimeFaces growl message with details of the selected record.
* @param {object} timeline - The timeline component.
* @param {object} growl - The growl component.
*/
function showRecordDetails(timeline, growl) {
var selection = timeline.getSelection();
if (selection.length) {
if (selection[0].row !== undefined) {
var record = timeline.getItem(selection[0].row);
growl.removeAll();
growl.renderMessage({
summary: record.group,
detail: getRecordDetails(record),
severity: "info"
});
}
}
}
/**
* Hides all growl messages and removes timeline selection.
* @param {object} timeline - The timeline component.
* @param {object} growl - The growl component.
*/
function hideMessages(timeline, growl) {
growl.removeAll();
timeline.setSelection(null);
}
/**
* Gets all the records that are fully or partially in the given time frame.
* @param {object} records - The object containing the records.
* @param {object} timeframe - The selected start and end time.
* @returns {object} - returns a list of matched records.
*/
function getRecordsInTimeframe(records, timeframe) {
var recordsIn = [];
$.each(records, function (i, record) {
if (record.className === "dummyRecord") {
return true;
} else if (record.start >= timeframe.start && record.start < timeframe.end) {
recordsIn.push(record);
} else if (record.end <= timeframe.end && record.end > timeframe.start) {
recordsIn.push(record);
} else if (record.start < timeframe.start && record.end > timeframe.end) {
recordsIn.push(record);
}
});
return recordsIn;
}
/**
* Gets the record details as a string.
* @param {object} record - The record object from the timeline component.
* @returns {string} - The details as a string value.
*/
function getRecordDetails(record) {
var details = "";
var start = toTimelineTime(record.start);
var end = toTimelineTime(record.end);
details += msg.sum_begin + ": " + convertMsToStr(start);
details += "<br/>";
details += msg.sum_end + ": " + convertMsToStr(end);
details += "<br/>";
details += msg.sum_duration + ": " + convertMsToUnits(end - start);
return details;
}
/**
* Get total duration of records of all categories in given time frame.
* @param {object} records - object containing the records.
* @param {object} timeframe - The selected start and end time.
* @returns {number} - duration of the records.
*/
function getDurationOfCategories(categories, timeframe) {
var duration = 0;
$.each(categories, function (category, records) {
duration += getDurationOfRecords(records, timeframe);
});
return duration;
}
/**
* Gets the duration of the given time frame.
* @param {object} timeframe - The selected start and end time.
* @returns {number} - duration of the observation's time frame.
*/
function getTimeframeDuration(timeframe) {
var rStartMs = toTimelineTime(timeframe.start);
var rEndMs = toTimelineTime(timeframe.end);
var start = (rStartMs > 0) ? rStartMs : 0;
var end = (rEndMs < OBSERVATION_DURATION) ? rEndMs : OBSERVATION_DURATION;
return end - start;
}
/**
* Gets the total duration of the records in the given time frame.
* @param {object} records - The object containing the records.
* @returns {number} - The duration of the records.
*/
function getDurationOfRecords(records, timeframe) {
var duration = 0;
$.each(records, function () {
var start = this.start;
var end = this.end;
if (this.className === "dummyRecord") {
return true;
}
if (start < timeframe.start) {
start = timeframe.start;
}
if (end > timeframe.end) {
end = timeframe.end;
}
if (end > start) {
duration += end - start;
}
});
return duration;
}
/**
* Converts the time in milliseconds to a string hh:mm:ss.
* @param {number} ms - The time in milliseconds.
* @returns {string} - The time in string as hh:mm:ss.
*/
function convertMsToStr(ms) {
var d = ms;
d = Math.floor(d / 1000);
var s = d % 60;
d = Math.floor(d / 60);
var m = d % 60;
d = Math.floor(d / 60);
var h = d % 60;
return [h, m, s].map(leadingZero).join(':');
}
/**
* Converts the time string in the form hh:mm:ss to milliseconds.
* @param {string} str - The time in a string as hh:mm:ss.
* @returns {number} - The time in milliseconds or NaN for unparseable time string.
*/
function convertStrToMs(str) {
var time = str.split(/:/);
// insert missing values
for (var i = 3 - time.length; i > 0; i--) {
time.unshift("0");
}
var seconds = 0;
for (var i = 0; i < time.length; i++) {
seconds += parseInt(time[i], 10) * Math.pow(60, 2 - i);
}
return seconds * 1000;
}
/**
* Converts the time in milliseconds to a string with the time units e.g. 1h 2m 0s.
* @param {number} ms - The time in milliseconds.
* @returns {string} - The time in string with units e.g. 1h 2m 0s.
*/
function convertMsToUnits(ms) {
var time = convertMsToStr(ms).split(":");
var units = "";
var getTimeUnit = function (i, unit) {
var n = parseInt(time[i], 10);
if (n > 0) {
units += n + unit;
}
};
if (ms <= 0) {
return "0 s";
}
if (ms < 1000) {
return "~1 s";
}
if (time.length === 3) {
getTimeUnit(0, " h");
getTimeUnit(1, " m");
getTimeUnit(2, " s");
} else {
return "0 s";
}
return units.replace(/([hms])(\d)/g, "$1 $2");
}
/**
* Returns the given number as a string and appends a leading zero
* to it if the number is a single digit number.
* @param {number} n - The given number.
* @returns {string} - number with possible leading zero.
*/
function leadingZero(n) {
return (n < 10 ? "0" + n : n.toString());
}
/**
* Calculates the percentage of two values.
* @param {number} a - The number of share.
* @param {number} b - The number of total quantity.
* @returns {number} - percentage ratio.
*/
function percentOf(a, b) {
if (a === 0 || b === 0) {
return 0;
}
return Math.round((a / b) * 100);
}
/**
* Gets the percentage of two values as a span element string.
* @param {number} a - The number of share.
* @param {number} b - The number of total quantity.
* @returns {string} - percent as span element string.
*/
function spanPercentOf(a, b) {
var percent = percentOf(a, b);
var str = " (" + percent.toString() + "%)";
if (percent < 10) {
str = " " + str;
}
if (percent < 100) {
str = " " + str;
}
return '<span class="percent">' + str + "</span>";
}
/**
* Gets the "zero" date with the time zone offset.
* @returns {date} - The zero date with the time zone offset.
*/
function getLocalZeroDate() {
var localDate = new Date(0);
var zeroDate = new Date(localDate.getTimezoneOffset() * 60 * 1000);
return zeroDate;
}
/**
* Converts the date object to the timeline component time.
* @param {date} date - The date object of the time to be converted.
* @returns {number} - The converted time in milliseconds.
*/
function toTimelineTime(date) {
return Math.abs(TIMELINE_BEGIN.getTime() - date.getTime());
}
/**
* Encodes HTML markup characters to HTML entities.
* @param {string} str - The string to be encoded.
* @returns {str} - The encoded string.
*/
function encodeHTML(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/**
* Checks if the user has scrolled to the bottom of the page.
* @param {number} padding - An extra padding to be checked.
* @return {boolean} - true if at bottom otherwise false.
*/
function isBottomOfDocument(padding) {
return $(window).scrollTop() >= $(document).height() - padding - $(window).height();
}