blob: 111f530a69fce615f95e97a728362cf2ce82a69e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
var htrace = htrace || {};
htrace.SearchResultsView = Backbone.View.extend({
// The minimum time span we will allow between begin and end.
MINIMUM_TIME_SPAN: 100,
begin: 0,
end: this.MINIMUM_TIME_SPAN,
initialize: function(options) {
this.searchResults = options.searchResults;
this.el = options.el;
this.listenTo(this.searchResults, 'add remove change reset', this.render);
// Re-render the canvas when the window size changes.
// Add a debouncer delay to avoid spamming render requests.
var view = this;
$(window).on("resize", _.debounce(function() {
view.render();
}, 250));
},
// Get the canvas X coordinate of a mouse click from the absolute event
// coordinate.
getCanvasX: function(e) {
return e.pageX - $("#resultsCanvas").offset().left;
},
// Get the canvas Y coordinate of a mouse click from the absolute event
// coordinate.
getCanvasY: function(e) {
return e.pageY - $("#resultsCanvas").offset().top;
},
handleMouseDown: function(e) {
e.preventDefault();
this.widgetManager.handle({
type: "mouseDown",
x: this.getCanvasX(e),
y: this.getCanvasY(e)
});
this.draw();
},
handleMouseUp: function(e) {
e.preventDefault();
this.widgetManager.handle({
type: "mouseUp",
x: this.getCanvasX(e),
y: this.getCanvasY(e)
});
this.draw();
},
handleMouseOut: function(e) {
e.preventDefault();
this.widgetManager.handle({
type: "mouseOut"
});
this.draw();
},
handleMouseMove: function(e) {
e.preventDefault();
this.widgetManager.handle({
type: "mouseMove",
x: this.getCanvasX(e),
y: this.getCanvasY(e)
});
this.draw();
},
render: function() {
console.log("SearchResultsView#render.");
$(this.el).html(_.template($("#search-results-view-template").html()));
$('#selectedTime').attr('readonly', 'readonly');
this.canvas = $("#resultsCanvas");
this.ctx = this.canvas.get(0).getContext("2d");
this.scaleCanvas();
this.setupCoordinates();
this.setupWidgets();
this.draw();
this.attachEvents();
return this;
},
/*
* Compute the ratio to use between the size of the canvas (i.e.
* canvas.ctx.width, canvas.ctx.height) and the size in "HTML5 pixels." Note
* that 'HTML5 pixels" don't actually correspond to screen pixels. A line 1
* "HTML5 pixel" wide actually takes up multiple scren pixels, etc.
*
* TODO: fix this to be sharper
*/
computeScaleFactor: function() {
var backingStoreRatio = this.ctx.backingStorePixelRatio ||
this.ctx.mozBackingStorePixelRatio ||
this.ctx.msBackingStorePixelRatio ||
this.ctx.webkitBackingStorePixelRatio ||
this.ctx.oBackingStorePixelRatio ||
this.ctx.backingStorePixelRatio || 1;
return (window.devicePixelRatio || 1) / backingStoreRatio;
},
// Sets up the canvas size and scaling.
scaleCanvas: function() {
var cssX = this.canvas.parent().innerWidth();
var cssY = $(window).innerHeight() - $("#header").innerHeight() - 50;
var ratio = this.computeScaleFactor();
console.log("scaleCanvas: cssX=" + cssX + ", cssY=" + cssY + ", ratio=" + ratio);
this.maxX = cssX;
this.maxY = cssY;
$('#searchView').css('height', cssY + "px");
$('#results').css('width', cssX + "px");
$('#results').css('height', cssY + "px");
$('#resultsView').css('width', cssX + "px");
$('#resultsView').css('height', cssY + "px");
$('#resultsDiv').css('width', cssX + "px");
$('#resultsDiv').css('height', cssY + "px");
$('#resultsCanvas').css('width', cssX + "px");
$('#resultsCanvas').css('height', cssY + "px");
this.ctx.canvas.width = cssX * ratio;
this.ctx.canvas.height = cssY * ratio;
this.ctx.scale(ratio, ratio);
},
//
// Set up the screen coordinates.
//
// 0 xB xD xS maxX
// +--------------+----------+--------------------+-----------+
// |ProcessId | Buttons | Span Description | Scrollbar |
// +--------------+----------+--------------------+-----------+
//
setupCoordinates: function() {
this.xB = Math.min(300, Math.floor(this.maxX / 5));
this.xD = this.xB + Math.min(75, Math.floor(this.maxX / 20));
var scrollBarWidth = Math.min(50, Math.floor(this.maxX / 10));
this.xS = this.maxX - scrollBarWidth;
},
setupWidgets: function() {
this.widgetManager = new htrace.WidgetManager({searchResultsView: this});
// Create a SpanWidget for each span we know about
var spanWidgetHeight = Math.min(25, Math.floor(this.maxY / 32));
var numResults = this.searchResults.size();
var groupY = 0;
for (var i = 0; i < numResults; i++) {
var widget = new htrace.SpanGroupWidget({
manager: this.widgetManager,
ctx: this.ctx,
span: this.searchResults.at(i),
x0: 0,
xB: this.xB,
xD: this.xD,
xF: this.xS,
y0: groupY,
begin: this.begin,
end: this.end,
spanWidgetHeight: spanWidgetHeight
});
groupY = widget.yF;
}
// Create the time cursor widget.
var selectedTime = this.begin;
if (this.timeCursor != null) {
selectedTime = this.timeCursor.selectedTime;
}
this.timeCursor = new htrace.TimeCursor({
manager: this.widgetManager,
selectedTime: selectedTime,
el: "#selectedTime"
});
this.timeCursor.ctx = this.ctx;
this.timeCursor.x0 = this.xD;
this.timeCursor.xF = this.xS;
this.timeCursor.y0 = 0;
this.timeCursor.yF = this.maxY;
this.timeCursor.begin = this.begin;
this.timeCursor.end = this.end;
},
draw: function() {
if (this.checkCanvasTooSmall()) {
return;
}
// Set the background to white.
this.ctx.save();
this.ctx.fillStyle="#ffffff";
this.ctx.strokeStyle="#000000";
this.ctx.fillRect(0, 0, this.maxX, this.maxY);
this.ctx.restore();
// Draw all the widgets.
this.widgetManager.handle({type: "draw"});
},
checkCanvasTooSmall: function() {
if ((this.maxX < 200) || (this.maxY < 200)) {
this.ctx.fillStyle="#cccccc";
this.ctx.strokeStyle="#000000";
this.ctx.fillRect(0, 0, this.maxX, this.maxY);
this.ctx.font = "24px serif";
this.ctx.fillStyle="#000000";
this.ctx.fillText("Canvas too small!", 0, 24);
return true;
}
return false;
},
attachEvents: function() {
// Use jquery to capture mouse events on the canvas.
// For some reason using backbone doesn't work for getting these events.
var view = this;
$("#resultsCanvas").off("mousedown");
$("#resultsCanvas").on("mousedown", function(e) {
view.handleMouseDown(e);
});
$("#resultsCanvas").off("mouseup");
$("#resultsCanvas").on("mouseup", function(e) {
view.handleMouseUp(e);
});
$("#resultsCanvas").off("mouseout");
$("#resultsCanvas").on("mouseout", function(e) {
view.handleMouseOut(e);
});
$("#resultsCanvas").off("mousemove");
$("#resultsCanvas").on("mousemove", function(e) {
view.handleMouseMove(e);
});
},
remove: function() {
$(window).off("resize");
$("#resultsCanvas").off("mousedown");
$("#resultsCanvas").off("mouseup");
$("#resultsCanvas").off("mousemove");
Backbone.View.prototype.remove.apply(this, arguments);
},
handleBeginOrEndChange: function(e, type) {
e.preventDefault();
var text = $(e.target).val().trim();
var d = null;
try {
d = htrace.parseDate(text);
} catch(err) {
$("#begin").val(htrace.dateToString(this.begin));
$("#end").val(htrace.dateToString(this.end));
htrace.showModalWarning("Timeline " + type + " Format Error",
"Please enter a valid time in the timeline " + type + " field.<p/>" +
err);
return null;
}
if (type === "begin") {
this.setBegin(d.valueOf());
} else if (type === "end") {
this.setEnd(d.valueOf());
} else {
throw "invalid type for handleBeginOrEndChange: expected begin or end.";
}
},
setBegin: function(val) {
if (this.end < val + this.MINIMUM_TIME_SPAN) {
this.begin = val;
this.end = val + this.MINIMUM_TIME_SPAN;
console.log("SearchResultsView#setBegin(begin=" + this.begin +
", end=" + this.end + ")");
$("#begin").val(htrace.dateToString(this.begin));
$("#end").val(htrace.dateToString(this.end));
} else {
this.begin = val;
console.log("SearchResultsView#setBegin(begin=" + this.begin + ")");
$("#begin").val(htrace.dateToString(this.begin));
}
this.render();
},
setEnd: function(val) {
if (this.begin + this.MINIMUM_TIME_SPAN > val) {
this.begin = val;
this.end = this.begin + this.MINIMUM_TIME_SPAN;
console.log("SearchResultsView#setEnd(begin=" + this.begin +
", end=" + this.end + ")");
$("#begin").val(htrace.dateToString(this.begin));
$("#end").val(htrace.dateToString(this.end));
} else {
this.end = val;
console.log("SearchResultsView#setEnd(end=" + this.end + ")");
$("#end").val(htrace.dateToString(this.end));
}
this.render();
},
zoomFitAll: function() {
var numResults = this.searchResults.size();
if (numResults == 0) {
this.setBegin(0);
this.setEnd(this.MINIMUM_TIME_SPAN);
return;
}
var minStart = 4503599627370496;
var maxEnd = 0;
for (var i = 0; i < numResults; i++) {
var span = this.searchResults.at(i);
var begin = span.getEarliestBegin();
if (begin < minStart) {
minStart = begin;
}
var end = span.getLatestEnd();
if (end > minStart) {
maxEnd = end;
}
}
this.setBegin(minStart);
this.setEnd(maxEnd);
},
// Apply a function to all spans
applyToAllSpans: function(cb) {
for (var i = 0; i < this.searchResults.length; i++) {
htrace.treeTraverseDepthFirstPre(this.searchResults.at(i),
htrace.getReifiedChildren, 0,
function(node, depth) {
console.log("node = " + node + ", node.constructor.name = " + node.constructor.name);
cb(node);
});
htrace.treeTraverseDepthFirstPre(this.searchResults.at(i),
htrace.getReifiedParents, 0,
function(node, depth) {
if (depth > 0) {
cb(node);
}
});
}
}
});