blob: 50e053f4a123b78b798b447a8d00ac8bc54d4c10 [file] [log] [blame]
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Touchpad Log Viewer</title>
<link type="text/css" href="css/smoothness/jquery-ui-1.8.16.custom.css" rel="stylesheet" />
<script src="js/jquery-1.6.4.min.js"></script>
<script src="js/jquery.mousewheel.js"></script>
<script src="js/jquery-ui-1.8.16.custom.min.js"></script>
<script src="js/bzip2.js"></script>
<script src="js/base64-binary.js"></script>
<script src="js/jsxcompressor.js"></script>
<script src="js/purl.js"></script>
<script src="finger_view_controller.js"></script>
<script src="graph_controller.js"></script>
<script src="secret_entries.js"></script>
<link rel="stylesheet" href="/style.css" type="text/css" media="all" />
<script type="text/javascript" charset="utf-8">
var finger_view_controller;
var current_layer = 0;
var json_obj;
var use_server = false;
var logname = undefined;
function update_range(event, ui) {
finger_view_controller.setRange(ui.values[0], ui.values[1]);
var begin_event = finger_view_controller.getEvent(ui.values[0]);
$("#begin_event").val(begin_event);
if ((beginTimestamp = finger_view_controller.getTimestamp(ui.values[0])) > 0)
$('#input_begin_time').val(beginTimestamp);
var end_event = finger_view_controller.getEvent(ui.values[1]);
$("#end_event").val(end_event);
endTimestamp = finger_view_controller.getPreviousHardwareStateTimestamp(
ui.values[1]);
if (endTimestamp > 0)
$('#input_end_time').val(endTimestamp);
var values = $("#slider").slider("option", "values");
$('#num_fts').text('Finger Touch Section (' +
finger_view_controller.getFTSIndex(values[1]) + ' / ' +
'0 ~ ' + (finger_view_controller.getNumFTS() - 1) + '): ');
}
function loadLogObj(obj, isNew) {
if (!isNew) {
var values = $("#slider").slider("option", "values");
var beginTime = finger_view_controller.getGETimestamp(values[0]);
var endTime = finger_view_controller.getLETimestamp(values[1]);
}
finger_view_controller.setEntriesLog(obj, current_layer);
finger_view_controller.initFTS();
var MAX_SIZE = 10000;
var min = Math.max(0, finger_view_controller.entries.length - MAX_SIZE);
var max = finger_view_controller.entries.length - 1;
if (isNew) {
var values = [min, max];
} else {
values[0] = finger_view_controller.getHardwareStateGETimestamp(beginTime);
values[1] = finger_view_controller.getHardwareStateLETimestamp(endTime);
}
$("#slider").slider({
min: min,
max: max,
values: values,
range: true,
slide: update_range,
change: update_range
});
update_range(null, { values: values });
finger_view_controller.resetZooms();
}
/**
* Converts raw input event stream from evdev input to the entries structure
* expected by finger_view_controller.js.
* @param {string} data The raw log file data in the following format:
* # Metadata in comment lines.
* # absinfo: [event_type] [min] [max] [fuzz] [flat] [resolution]
* E: [timestamp] [event_type] [event_code] [event_value] [slot]
* @return {Object} Event structure expected by loadLogObj.
*/
function convertEventStreamToEntries(data) {
// Clone an object by serializing and parsing it.
var clone = function(obj) {
return JSON.parse(JSON.stringify(obj));
}
// Default hardware properties.
var obj = {
"entries": [],
"hardwareProperties": {
"bottom": 1000.0,
"left": 0.0,
"right": 2000.0,
"top": 0.0,
"xResolution": 1.0,
"yResolution": 1.0},
"properties": {
"Pressure Calibration Offset": 0.0,
"Pressure Calibration Slope": 1.0,
}};
var evt = {
"fingers": [],
"timestamp": 0,
"touchCount": 0,
"type": "hardwareState",
};
var eventCodes = {
'002f': 'slot',
'0030': 'touchMajor',
'0035': 'positionX',
'0036': 'positionY',
'0039': 'trackingId',
'003a': 'pressure',
};
var ignoreCodes = {
'0000': 'abs_x',
'0001': 'abs_y',
'0018': 'abs_pressure',
};
var slotDefaults = {
'flags': 0,
'trackingId': -1,
};
var slots = [];
var slot = -1;
var lines = data.split('\n');
for (var i = 0; i < lines.length; i++) {
if (lines[i].slice(0, 10) == '# absinfo:') {
var val = lines[i].split(' ');
switch (val[2]) {
case '0':
obj.hardwareProperties.left = parseInt(val[3]);
obj.hardwareProperties.right = parseInt(val[4]) + 1;
obj.hardwareProperties.xResolution = parseInt(val[7]);
break;
case '1':
obj.hardwareProperties.top = parseInt(val[3]);
obj.hardwareProperties.bottom = parseInt(val[4]) + 1;
obj.hardwareProperties.yResolution = parseInt(val[7]);
break;
case '47': // Slot.
var maxSlot = parseInt(val[4]);
while (slots.length <= maxSlot)
slots.push(clone(slotDefaults));
break;
}
} else if (lines[i].slice(0, 3) == 'E: ') {
var val = lines[i].split(' ');
switch (val[2]) { // Event type.
case '0000': // EV_SYN: Clone current hardware state and push to list.
// First copy slots into fingers.
evt.fingers = [];
for (var j = 0; j < slots.length; j++) {
if (slots[j].trackingId >= 0)
evt.fingers.push(slots[j]);
}
evt.touchCount = evt.fingers.length;
evt.timestamp = parseFloat(val[1]);
obj.entries.push(clone(evt));
break;
case '0003': // EV_ABS: Update hardware state.
if (eventCodes[val[3]]) {
// Parse slot identifier on event info line.
if (val.length > 5)
slot = parseInt(val[5]);
if (eventCodes[val[3]] == 'slot') {
slot = parseInt(val[4]);
} else if (slot >= 0) {
// Initialize the requested slot if not set.
if (!slots[slot])
slots[slot] = clone(slotDefaults);
// Make up tracking ID if we miss the start of a touch.
if (eventCodes[val[3]] != 'trackingId' &&
slots[slot].trackingId == -1)
slots[slot].trackingId = slot + 1;
slots[slot][eventCodes[val[3]]] = parseInt(val[4]);
}
} else if (!ignoreCodes[val[3]]) {
console.log('Unrecognized event type ' + val[2] +
' code ' + val[3]);
}
break;
}
}
}
// For touch screens the absolute value is more useful than the scaled value.
obj.hardwareProperties.xResolution = 1.0;
obj.hardwareProperties.yResolution = 1.0;
return obj;
}
function loadLog(obj) {
if (!obj) {
alert('Unrecognized input file');
return;
}
json_obj = obj;
generateRadioButtons(obj);
current_layer = 0;
loadLogObj(obj, true);
}
function handleFileSelect(evt) {
$('#text_box').removeClass('text_box_hilight');
evt.stopPropagation();
evt.preventDefault();
var files = evt.dataTransfer.files; // FileList object.
var file = files[0];
var reader = new FileReader();
reader.onload = function(e) {
var obj;
$('#text_box').val(e.target.result);
if (e.target.result[0] == '#')
obj = convertEventStreamToEntries(e.target.result);
else if (e.target.result[0] == '{')
obj = jQuery.parseJSON(e.target.result);
loadLog(obj);
};
reader.readAsText(file);
}
var count = 0;
function handleDragOver(evt) {
evt.stopPropagation();
evt.preventDefault();
}
function handleDragEnter(evt) {
$('#text_box').addClass('text_box_hilight');
evt.stopPropagation();
evt.preventDefault();
}
function handleDragOut(evt) {
$('#text_box').removeClass('text_box_hilight');
evt.stopPropagation();
evt.preventDefault();
}
function setup() {
var dropZone = document.getElementById('text_box');
dropZone.addEventListener('dragenter', handleDragEnter, false);
dropZone.addEventListener('dragleave', handleDragOut, false);
dropZone.addEventListener('dragover', handleDragOver, false);
dropZone.addEventListener('drop', handleFileSelect, false);
}
// fix layerX, layerY warnings
(function(){
// remove layerX and layerY
var all = $.event.props,
len = all.length,
res = [];
while (len--) {
var el = all[len];
if (el != 'layerX' && el != 'layerY') res.push(el);
}
$.event.props = res;
}());
function begin_stepBack() {
var values = $("#slider").slider("option", "values");
var minValue = $("#slider").slider("option", "min");
if (values[0] > minValue) {
var newValue = finger_view_controller.prevHardwareState(values[0]);
if (newValue < 0)
return;
values[0] = newValue;
$("#slider").slider("option", "values", values);
$('#input_begin_time').val(finger_view_controller.getTimestamp(newValue));
}
}
function begin_stepForward() {
var values = $("#slider").slider("option", "values");
if (values[0] < values[1]) {
var newValue = finger_view_controller.nextHardwareState(values[0]);
if (newValue < 0)
return;
values[0] = newValue;
$("#slider").slider("option", "values", values);
$('#input_begin_time').val(finger_view_controller.getTimestamp(newValue));
}
}
function end_stepBack() {
var values = $("#slider").slider("option", "values");
if (values[1] > values[0]) {
var newValue = finger_view_controller.prevHardwareState(values[1]);
if (newValue < 0)
return;
values[1] = newValue;
$("#slider").slider("option", "values", values);
$('#input_end_time').val(finger_view_controller.getTimestamp(newValue));
}
}
function end_stepForward() {
var values = $("#slider").slider("option", "values");
var maxValue = $("#slider").slider("option", "max");
if (values[1] < maxValue) {
var newValue = finger_view_controller.nextHardwareState(values[1]);
if (newValue < 0)
return;
values[1] = newValue;
$("#slider").slider("option", "values", values);
$('#input_end_time').val(finger_view_controller.getTimestamp(newValue));
}
}
function setBeginTimestamp() {
timestamp = $('#input_begin_time').val();
var values = $('#slider').slider('option', 'values');
var minValue = $('#slider').slider('option', 'min');
var maxValue = $('#slider').slider('option', 'max');
var newValue = finger_view_controller.getHardwareStateLETimestamp(timestamp);
if (newValue >= minValue && newValue <= maxValue) {
values[0] = (newValue >= values[1] ? values[1] : newValue);
$('#slider').slider('option', 'values', values);
}
}
function setEndTimestamp() {
timestamp = $('#input_end_time').val();
var values = $('#slider').slider('option', 'values');
var minValue = $('#slider').slider('option', 'min');
var maxValue = $('#slider').slider('option', 'max');
var newValue = finger_view_controller.getHardwareStateGETimestamp(timestamp);
if (newValue >= minValue && newValue <= maxValue) {
values[1] = (newValue <= values[0] ? values[0] : newValue);
$('#slider').slider('option', 'values', values);
}
}
function gotoPrevFTS() {
var values = $('#slider').slider('option', 'values');
var fts = finger_view_controller.getPrevFTS(values);
$('#slider').slider('option', 'values', fts);
}
function gotoNextFTS() {
var values = $('#slider').slider('option', 'values');
var fts = finger_view_controller.getNextFTS(values);
$('#slider').slider('option', 'values', fts);
}
function gotoFirstFTS() {
var fts = finger_view_controller.getFirstFTS();
$('#slider').slider('option', 'values', fts);
}
function gotoLastFTS() {
var fts = finger_view_controller.getLastFTS();
$('#slider').slider('option', 'values', fts);
}
function expandAllFTS() {
var values = finger_view_controller.getAllEntries();
$('#slider').slider('option', 'values', values);
}
function shrink() {
var values = $("#slider").slider("option", "values");
var snippet = finger_view_controller.getSnippet(values[0], values[1]);
$('#text_box').val(JSON.stringify(snippet, null, 2));
loadLogObj(snippet, true);
if (use_server) {
$.post('/save/' + logname, JSON.stringify(snippet, null, 2), function () {
alert("Saved");
});
}
}
function generateRadioButtons(obj) {
var radioHTML = '<input type="radio" name="viewLayer" value="0" ' +
'checked="checked" onclick="radioChange(event)">' +
obj.interpreterName + '</input><br/>';
var layer = 0;
while (obj.hasOwnProperty('nextLayer')) {
obj = obj.nextLayer;
layer++;
radioHTML += '<input type="radio" name="viewLayer" value="' + layer +
'" onclick="radioChange(event)">' + obj.interpreterName +
'</input><br/>';
}
document.getElementById('button_div').innerHTML = radioHTML;
if (layer > 1) {
$("#button_div").css("visibility", "visible");
} else {
$("#button_div").css("visibility", "hidden");
}
}
function generateUnittest() {
var interpreter_name = $('#unittest_interpreter').val();
var unittest_name = $('#unittest_testname').val();
var values = $('#slider').slider('option', 'values');
var unittest = finger_view_controller.getUnitTest(
values[0], values[1], interpreter_name, unittest_name);
$('#unittest_box').val(unittest);
}
function radioChange(e) {
if (e.target.value != current_layer) {
current_layer = e.target.value;
loadLogObj(json_obj, false);
}
}
function extractActivityLog(tar) {
// a tar file is a sequence of 512 byte blocks. Starting with a header block
// followed by the contents of that file, repeated for as many files
// as are in the archive. The file contents are padded if zeros to fill up
// multiples of 512 byte blocks.
// extracts name from tar file header
function getName(header_offset) {
name_array = new Uint8Array(tar.slice(header_offset, header_offset + 100));
if (name_array[0] == 0) {
return undefined;
}
return String.fromCharCode.apply(null, name_array);
}
// extracts length from tar file header
function getLength(header_offset) {
start = header_offset + 124;
length_array = new Uint8Array(tar.slice(start, start + 12));
length_string = String.fromCharCode.apply(null, length_array);
return parseInt(length_string, 8);
}
// iterate over all files and collect all activity logs
var header_offset = 0;
var log_list = new Array();
var name_pattern = /touchpad_activity_([0-9\-]+)/;
while (header_offset + 512 < tar.byteLength) {
// cannot read a name from the header? -> End of archive.
var name = getName(header_offset);
if (name == undefined)
break;
var length = getLength(header_offset);
if (name_pattern.test(name)) {
// store file info in list
result = new Object();
result.name = name;
result.timestamp = name_pattern.exec(name)[1];
result.offset = header_offset + 512
result.length = length;
log_list.push(result)
}
// Next header starts after the file on the next 512 byte aligned
// block.
header_offset = Math.ceil((header_offset+length) / 512 + 1) * 512;
}
// sort by descending timestamps to find latest file
log_list.sort(function(a, b) {
return a.timestamp < b.timestamp;
});
log = log_list[0];
return tar.slice(log.offset, log.offset + log.length);
}
$(document).ready(function() {
setup();
$("#playpause").button({
text: false,
icons: {
primary: "ui-icon-play"
}
});
var canvas = document.getElementById('fview');
var gc = new GraphController($('#gview'));
gc.setLineSegments([{'start': {'xPos': 0.1, 'yPos': 0.1},
'end': {'xPos': 0.9, 'yPos': 0.5}}]);
var inGc = new GraphController($('#fview'));
finger_view_controller = new FingerViewController(inGc, gc, $('#intext'),
$('#out-lock-head'));
$("#out-resetzoom").button({
icons: {
primary: "ui-icon-arrow-4-diag"
}
}).click(function() {
gc.animResetZoom();
});
$("#in-resetzoom").button({
icons: {
primary: "ui-icon-arrow-4-diag"
}
}).click(function() {
inGc.animResetZoom();
});
$("#begin_stepback").button({
text: false,
icons: {
primary: "ui-icon-triangle-1-w"
}
}).click(begin_stepBack);
$("#begin_stepforward").button({
text: false,
icons: {
primary: "ui-icon-triangle-1-e"
}
}).click(begin_stepForward);
$("#end_stepback").button({
text: false,
icons: {
primary: "ui-icon-triangle-1-w"
}
}).click(end_stepBack);
$("#end_stepforward").button({
text: false,
icons: {
primary: "ui-icon-triangle-1-e"
}
}).click(end_stepForward);
$("#shrink").button({
text: true,
}).click(shrink);
$("#prev_finger_touch").button({
text: true,
}).click(gotoPrevFTS);
$("#next_finger_touch").button({
text: true,
}).click(gotoNextFTS);
$("#first_finger_touch").button({
text: true,
}).click(gotoFirstFTS);
$("#last_finger_touch").button({
text: true,
}).click(gotoLastFTS);
$("#all_finger_touch").button({
text: true,
}).click(expandAllFTS);
$("#unittest").button({
text: true,
}).click(generateUnittest);
$(window).keypress(function (event) {
switch (event.keyCode) {
case 44: // ',' move the slider to previous fts
gotoPrevFTS();
break;
case 46: // '.' move the slider to next fts
gotoNextFTS();
break;
case 109: // 'm' move the slider to the first fts
gotoFirstFTS();
break;
case 47: // '/' move the slider to the last fts
gotoLastFTS();
break;
case 97: // 'a' expand the slider to all entries
expandAllFTS();
break;
case 107: // 'k'
end_stepBack();
break;
case 106: // 'j'
end_stepForward();
break;
case 103: // 'g'
begin_stepBack();
break;
case 104: // 'h'
begin_stepForward();
break;
case 115: // 's'
shrink();
break;
}
});
$('#bzipupload').change(function () {
// read selected file
reader = new FileReader();
reader.onload = function (event) {
// extract feedback.bz2
var feedback_bz2 = new Uint8Array(event.target.result);
var feedback = bzip2.simple(bzip2.array(feedback_bz2));
// extract touchpad_activity_log.tar' from feedback
var start_key = 'hack-33025-touchpad_activity="""\nbegin-base64 644 touchpad_activity_log.tar';
var start_idx = feedback.indexOf(start_key) + start_key.length + 1;
var end_idx = feedback.indexOf('====', start_idx) -1;
var touchpad_tar_base64 = feedback.substring(start_idx, end_idx);
// decode base64
var touchpad_tar = Base64Binary.decodeArrayBuffer(touchpad_tar_base64);
// extract log.gz from touchpad.tar
var log_gz = extractActivityLog(touchpad_tar);
// extract log.gz
var log = (new JXG.Util.Unzip(new Uint8Array(log_gz))).unzip()[0][0];
// parse as json and load
var log_obj = jQuery.parseJSON(log);
loadLog(log_obj);
};
reader.readAsArrayBuffer(document.getElementById('bzipupload').files[0]);
});
logname = $.url().fparam('id');
use_server = logname !== '';
if (use_server) {
$.get('/load/' + logname, function(data) {
loadLog(data);
}, 'json');
} else {
loadLog(secret_obj);
}
});
</script>
</head>
<body>
<div id="container">
<h2>Touchpad Activity Viewer</h2>
<div id="viewer">
<div class="viewspan">
<h3>Input</h3>
<canvas class="view" id="fview" width="480" height="320"></canvas><br/>
<button class="button" id="in-resetzoom">Reset Zoom</button>
<div id="intext">Time: 123123123.123<br/>Finger Cnt: 2<br/>Touch Cnt: 2</div>
</div>
<div class="viewspan">
<h3>Output</h3>
<canvas class="view" id="gview" width="480" height="320"></canvas><br/>
<button class="button" id="out-resetzoom">Reset Zoom</button>
<input type="checkbox" id="out-lock-head" />
<label for="out-lock-head">Lock Head Location</label>
</div>
<div class="midviewspan" id="button_div"></div>
</div>
<div id="playbar">
<h2>Time Range</h2>
<button id="playpause">Play</button>
<button id="shrink">Shrink Range</button>
<div id="sliderdiv"><div id="slider"></div></div>
<div id="begin">
<h3>Begin Time</h3>
<button id="begin_stepback">Begin Step Back</button>
<button id="begin_stepforward">Begin Step Forward</button>
Set begin time: <input type='text' id='input_begin_time'
onkeyup="setBeginTimestamp()" />
<br/><textarea rows="4" id="begin_event" readonly="readonly"></textarea>
</div>
<div id="end">
<h3>End Time</h3>
<button id="end_stepback">End Step Back</button>
<button id="end_stepforward">End Step Forward</button>
Set end time: <input type='text' id='input_end_time'
onkeyup="setEndTimestamp()" />
<br/><textarea rows="4" id="end_event" readonly="readonly"></textarea>
</div>
<div id="touchselect">
<span id='num_fts'></span>
<button id="first_finger_touch"> &lt;&lt; </button>
<button id="prev_finger_touch"> &lt; </button>
<button id="all_finger_touch"> all </button>
<button id="next_finger_touch"> &gt; </button>
<button id="last_finger_touch"> &gt;&gt; </button>
</div>
</div>
<div id="load">
<h2>Load/Save Activity Log</h2>
<div id="textdiv">
<h3>From Text File</h3>
<textarea id="text_box">Drag and drop a touchpad log file here.</textarea></div>
<div id="feedbackupload">
<h3>From Feedback Report</h3>
Load from Feedback Report: <input type="file" id="bzipupload"></input>
</div>
<br style="clear: both;"/>
</div>
<div id="unittest_div">
<input id="unittest_interpreter" value="InterpreterName"/>
<input id="unittest_testname" value="TestName"/>
<button id="unittest">Generate Unittest</button>
<textarea rows="24" cols="80" id="unittest_box"></textarea>
</div>
</div>
</body>
</html>