HTRACE-186. gui: support finding the parents and children of spans, add owl (cmccabe)
diff --git a/htrace-htraced/src/web/app/query_results.js b/htrace-htraced/src/web/app/query_results.js
index 6fdde9f..dc37e1e 100644
--- a/htrace-htraced/src/web/app/query_results.js
+++ b/htrace-htraced/src/web/app/query_results.js
@@ -20,7 +20,7 @@
 var htrace = htrace || {};
 
 htrace.QueryResults = Backbone.Collection.extend({
-  // The query results are spans. 
+  // The query results are spans.
   model: htrace.Span,
 
   initialize: function(options) {
diff --git a/htrace-htraced/src/web/app/search_result.js b/htrace-htraced/src/web/app/search_result.js
new file mode 100644
index 0000000..9798ad7
--- /dev/null
+++ b/htrace-htraced/src/web/app/search_result.js
@@ -0,0 +1,55 @@
+/*
+ * 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 || {};
+
+// A pair of span trees: one going up, and the other going down.
+// This represents a single search result.
+htrace.SearchResult = Backbone.Model.extend({
+  initialize: function(options) {
+    this.set("childrenRoot", {
+      root: options.span,
+      contents: null,
+    });
+    this.set("childrenRoot", {
+      root: options.span,
+      contents: null,
+    });
+
+    this.set("parentsRoot", options.span);
+  },
+
+  getBegin: function() {
+    var begin = this.get("span").get("begin");
+    var children = this.get("children");
+    for (var childIdx = 0; childIdx < children.length; childIdx++) {
+      begin = Math.min(begin, children[childIdx].getBegin());
+    }
+    return begin;
+  },
+
+  getEnd: function() {
+    var end = this.get("span").get("end");
+    var children = this.get("children");
+    for (var childIdx = 0; childIdx < children.length; childIdx++) {
+      end = Math.max(end, children[childIdx].getEnd());
+    }
+    return end;
+  }
+});
diff --git a/htrace-htraced/src/web/app/search_results.js b/htrace-htraced/src/web/app/search_results.js
index d214918..25b18ae 100644
--- a/htrace-htraced/src/web/app/search_results.js
+++ b/htrace-htraced/src/web/app/search_results.js
@@ -20,6 +20,6 @@
 var htrace = htrace || {};
 
 htrace.SearchResults = Backbone.Collection.extend({
-  // The search results are spans. 
-  model: htrace.Span
+  // The search results are span trees.
+  model: htrace.SpanTreeNode
 });
diff --git a/htrace-htraced/src/web/app/search_results_view.js b/htrace-htraced/src/web/app/search_results_view.js
index b3473c4..111f530 100644
--- a/htrace-htraced/src/web/app/search_results_view.js
+++ b/htrace-htraced/src/web/app/search_results_view.js
@@ -27,12 +27,10 @@
 
   end: this.MINIMUM_TIME_SPAN,
 
-  focused: false,
-
   initialize: function(options) {
-    this.model = options.searchResults;
+    this.searchResults = options.searchResults;
     this.el = options.el;
-    this.listenTo(this.model, 'add remove change reset', this.render);
+    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.
@@ -56,50 +54,40 @@
 
   handleMouseDown: function(e) {
     e.preventDefault();
-    var x = this.getCanvasX(e);
-    var y = this.getCanvasY(e);
-    var focused = this.widgetManager.handleMouseDown(x, y);
-    if (focused != this.focused) {
-      this.draw();
-      this.focused = focused;
-    }
+    this.widgetManager.handle({
+      type: "mouseDown",
+      x: this.getCanvasX(e),
+      y: this.getCanvasY(e)
+    });
+    this.draw();
   },
 
   handleMouseUp: function(e) {
     e.preventDefault();
-    var x = this.getCanvasX(e);
-    var y = this.getCanvasY(e);
-    this.widgetManager.handleMouseUp(x, y);
-    this.focused = false;
+    this.widgetManager.handle({
+      type: "mouseUp",
+      x: this.getCanvasX(e),
+      y: this.getCanvasY(e)
+    });
     this.draw();
   },
 
-  // When the mouse leaves the canvas, treat it like a mouse up event at -1, -1
-  // if something is focused.
   handleMouseOut: function(e) {
-    if (this.focused) {
-      this.widgetManager.handleMouseUp(-1, -1);
-      this.focused = false;
-      this.draw();
-    }
+    e.preventDefault();
+    this.widgetManager.handle({
+      type: "mouseOut"
+    });
+    this.draw();
   },
 
   handleMouseMove: function(e) {
     e.preventDefault();
-    var x = this.getCanvasX(e);
-    var y = this.getCanvasY(e);
-    if (this.focused) {
-      var mustDraw = false;
-      if (this.widgetManager.handleMouseMove(x, y)) {
-        mustDraw = true;
-      }
-    }
-    if (this.timeCursor.handleMouseMove(x, y)) {
-      mustDraw = true;
-    }
-    if (mustDraw) {
-      this.draw();
-    }
+    this.widgetManager.handle({
+      type: "mouseMove",
+      x: this.getCanvasX(e),
+      y: this.getCanvasY(e)
+    });
+    this.draw();
   },
 
   render: function() {
@@ -110,7 +98,6 @@
     this.ctx = this.canvas.get(0).getContext("2d");
     this.scaleCanvas();
     this.setupCoordinates();
-    this.setupTimeCursor();
     this.setupWidgets();
     this.draw();
     this.attachEvents();
@@ -160,66 +147,59 @@
   //
   // Set up the screen coordinates.
   //
-  //  0              buttonX    descX                scrollX    maxX
+  //  0              xB         xD                   xS         maxX
   //  +--------------+----------+--------------------+-----------+
   //  |ProcessId     | Buttons  | Span Description   | Scrollbar |
   //  +--------------+----------+--------------------+-----------+
   //
   setupCoordinates: function() {
-    this.buttonX = Math.min(300, Math.floor(this.maxX / 5));
-    this.descX = this.buttonX + Math.min(75, Math.floor(this.maxX / 20));
+    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.scrollX = this.maxX - scrollBarWidth;
-  },
-
-  setupTimeCursor: function() {
-    var selectedTime;
-    if (this.timeCursor != null) {
-      selectedTime = this.timeCursor.selectedTime;
-      console.log("setupTimeCursor: selectedTime = (prev) " + selectedTime);
-    } else {
-      selectedTime = this.begin;
-      console.log("setupTimeCursor: selectedTime = (begin) " + selectedTime);
-    }
-    this.timeCursor = new htrace.TimeCursor({
-      ctx: this.ctx,
-      x0: this.descX,
-      xF: this.scrollX,
-      el: "#selectedTime",
-      y0: 0,
-      yF: this.maxY,
-      begin: this.begin,
-      end: this.end,
-      selectedTime: selectedTime
-    });
+    this.xS = this.maxX - scrollBarWidth;
   },
 
   setupWidgets: function() {
-    var widgets = [];
-    var spanWidgetHeight = Math.min(25, Math.floor(this.maxY / 32));
+    this.widgetManager = new htrace.WidgetManager({searchResultsView: this});
 
     // Create a SpanWidget for each span we know about
-    var numSpans = this.model.size();
-    for (var i = 0; i < numSpans; i++) {
-      var spanWidget = new htrace.SpanWidget({
+    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.model.at(i),
+        span: this.searchResults.at(i),
         x0: 0,
-        xB: this.buttonX,
-        xD: this.descX,
-        xF: this.scrollX,
-        y0: i * spanWidgetHeight,
-        yF: (i * spanWidgetHeight) + (spanWidgetHeight - 1),
+        xB: this.xB,
+        xD: this.xD,
+        xF: this.xS,
+        y0: groupY,
         begin: this.begin,
-        end: this.end
+        end: this.end,
+        spanWidgetHeight: spanWidgetHeight
       });
-      widgets.push(spanWidget);
+      groupY = widget.yF;
     }
 
-    // Create a new root-leve WidgetManager
-    this.widgetManager = new htrace.WidgetManager({
-      widgets: widgets
+    // 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() {
@@ -227,7 +207,7 @@
       return;
     }
 
-    // Set the background to white. 
+    // Set the background to white.
     this.ctx.save();
     this.ctx.fillStyle="#ffffff";
     this.ctx.strokeStyle="#000000";
@@ -235,8 +215,7 @@
     this.ctx.restore();
 
     // Draw all the widgets.
-    this.widgetManager.draw();
-    this.timeCursor.draw();
+    this.widgetManager.handle({type: "draw"});
   },
 
   checkCanvasTooSmall: function() {
@@ -268,10 +247,6 @@
     $("#resultsCanvas").on("mouseout", function(e) {
       view.handleMouseOut(e);
     });
-    $(window).off("mouseup");
-    $(window).on("mouseup"), function(e) {
-      view.handleGlobalMouseUp(e);
-    }
     $("#resultsCanvas").off("mousemove");
     $("#resultsCanvas").on("mousemove", function(e) {
       view.handleMouseMove(e);
@@ -342,24 +317,45 @@
   },
 
   zoomFitAll: function() {
-    var numSpans = this.model.size();
-    if (numSpans == 0) {
+    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 < numSpans; i++) {
-      var span = this.model.at(i);
-      if (span.get('begin') < minStart) {
-        minStart = span.get('begin');
+    for (var i = 0; i < numResults; i++) {
+      var span = this.searchResults.at(i);
+      var begin = span.getEarliestBegin();
+      if (begin < minStart) {
+        minStart = begin;
       }
-      if (span.get('end') > maxEnd) {
-        maxEnd = span.get('end');
+      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);
+            }
+          });
+    }
   }
 });
diff --git a/htrace-htraced/src/web/app/span.js b/htrace-htraced/src/web/app/span.js
index 2c06fa0..a056b4f 100644
--- a/htrace-htraced/src/web/app/span.js
+++ b/htrace-htraced/src/web/app/span.js
@@ -22,6 +22,46 @@
 // The invalid span ID, which is all zeroes.
 htrace.INVALID_SPAN_ID = "0000000000000000";
 
+// Convert an array of htrace.Span models into a comma-separated string.
+htrace.spanModelsToString = function(spans) {
+  var ret = "";
+  var prefix = "";
+  for (var i = 0; i < spans.length; i++) {
+    ret += prefix + JSON.stringify(spans[i].unparse());
+    prefix = ", ";
+  }
+  return ret;
+};
+
+// Convert an array of return results from ajax calls into an array of
+// htrace.Span models.
+htrace.parseMultiSpanAjaxQueryResults = function(ajaxCalls) {
+  var parsedSpans = [];
+  for (var i = 0; i < ajaxCalls.length; i++) {
+    var text = ajaxCalls[i][0];
+    var result = ajaxCalls[i][1];
+    if (ajaxCalls[i]["status"] != "200") {
+      throw "ajax error: " + ajaxCalls[i].statusText;
+    }
+    var parsedSpan = new htrace.Span({});
+    try {
+      parsedSpan.parse(ajaxCalls[i].responseJSON, {});
+    } catch (e) {
+      throw "span parse error: " + e;
+    }
+    parsedSpans.push(parsedSpan);
+  }
+  return parsedSpans;
+};
+
+htrace.getReifiedParents = function(span) {
+  return span.get("reifiedParents") || [];
+};
+
+htrace.getReifiedChildren = function(span) {
+  return span.get("reifiedChildren") || [];
+};
+
 htrace.Span = Backbone.Model.extend({
   // Parse a span sent from htraced.
   // We use more verbose names for some attributes.
@@ -36,6 +76,16 @@
     this.set("description", response.d ? response.d : "");
     this.set("begin", response.b ? parseInt(response.b, 10) : 0);
     this.set("end", response.e ? parseInt(response.e, 10) : 0);
+
+    this.set("selected", false);
+
+    // reifiedChildren starts off as null and will be filled in as needed.
+    this.set("reifiedChildren", null);
+
+    // If there are parents, reifiedParents starts off as null.  Otherwise, we
+    // know it is the empty array.
+    this.set("reifiedParents", (this.get("parents").length == 0) ? [] : null);
+
     return span;
   },
 
@@ -65,5 +115,138 @@
       obj.e = this.get("end");
     }
     return obj;
-  }
+  },
+
+  //
+  // Although the parent IDs are always present in the 'parents' field of the
+  // span, sometimes we need the actual parent span models.  In that case we
+  // must "reify" them (make them real).
+  //
+  // This functionReturns a jquery promise which reifies all the parents of this
+  // span and stores them into reifiedParents.  The promise returns the empty
+  // string on success, or an error string on failure.
+  //
+  reifyParents: function() {
+    var span = this;
+    var numParents = span.get("parents").length;
+    var ajaxCalls = [];
+    // Set up AJAX queries to reify the parents.
+    for (var i = 0; i < numParents; i++) {
+      ajaxCalls.push($.ajax({
+        url: "span/" + span.get("parents")[i],
+        data: {},
+        contentType: "application/json; charset=utf-8",
+        dataType: "json"
+      }));
+    }
+    var rootDeferred = jQuery.Deferred();
+    $.when.apply($, ajaxCalls).then(function() {
+      var reifiedParents = [];
+      try {
+        reifiedParents = htrace.parseMultiSpanAjaxQueryResults(ajaxCalls);
+      } catch (e) {
+        rootDeferred.resolve("Error reifying parents for " +
+            span.get("spanId") + ": " + e);
+        return;
+      }
+      // The current span is a child of the reified parents.  There may be other
+      // children of those parents, but we are ignoring that here.  By making
+      // this non-null, the "expand children" button will not appear for these
+      // paren spans.
+      for (var j = 0; j < reifiedParents.length; j++) {
+        reifiedParents[j].set("reifiedChildren", [span]);
+      }
+      console.log("Setting reified parents for " + span.get("spanId") +
+          " to " + htrace.spanModelsToString (reifiedParents));
+      span.set("reifiedParents", reifiedParents);
+      rootDeferred.resolve("");
+    });
+    return rootDeferred.promise();
+  },
+
+  //
+  // The span itself does not contain its children.  However, the server has an
+  // index which can be used to easily find the children of a particular span.
+  //
+  // This function returns a jquery promise which reifies all the children of
+  // this span and stores them into reifiedChildren.  The promise returns the
+  // empty string on success, or an error string on failure.
+  //
+  reifyChildren: function() {
+    var rootDeferred = jQuery.Deferred();
+    var span = this;
+    $.ajax({
+        url: "span/" + span.get("spanId") + "/children?lim=50",
+        data: {},
+        contentType: "application/json; charset=utf-8",
+        dataType: "json"
+      }).done(function(childIds) {
+        var ajaxCalls = [];
+        for (var i = 0; i < childIds.length; i++) {
+          ajaxCalls.push($.ajax({
+            url: "span/" + childIds[i],
+            data: {},
+            contentType: "application/json; charset=utf-8",
+            dataType: "json"
+          }));
+        };
+        $.when.apply($, ajaxCalls).then(function() {
+          var reifiedChildren;
+          try {
+            reifiedChildren = htrace.parseMultiSpanAjaxQueryResults(ajaxCalls);
+          } catch (e) {
+            reifiedChildren = rootDeferred.resolve("Error reifying children " +
+                "for " + span.get("spanId") + ": " + e);
+            return;
+          }
+          // The current span is a parent of the new child.
+          // There may be other parents, but we are ignoring that here.
+          // By making this non-null, the "expand parents" button will not
+          // appear for these child spans.
+          for (var j = 0; j < reifiedChildren.length; j++) {
+            reifiedChildren[j].set("reifiedParents", [span]);
+          }
+          console.log("Setting reified children for " + span.get("spanId") +
+              " to " + htrace.spanModelsToString (reifiedChildren));
+          span.set("reifiedChildren", reifiedChildren);
+          rootDeferred.resolve("");
+        });
+      }).fail(function(statusData) {
+        // Check if the /children query failed.
+        rootDeferred.resolve("Error querying children of " +
+            span.get("spanId") + ": got " + statusData);
+        return;
+      });
+    return rootDeferred.promise();
+  },
+
+  // Get the earliest begin time of this span or any of its reified parents or
+  // children.
+  getEarliestBegin: function() {
+    var earliestBegin = this.get("begin");
+    htrace.treeTraverseDepthFirstPre(this, htrace.getReifiedParents, 0,
+        function(span, depth) {
+          earliestBegin = Math.min(earliestBegin, span.get("begin"));
+        });
+    htrace.treeTraverseDepthFirstPre(this, htrace.getReifiedChildren, 0,
+        function(span, depth) {
+          earliestBegin = Math.min(earliestBegin, span.get("begin"));
+        });
+    return earliestBegin;
+  },
+
+  // Get the earliest begin time of this span or any of its reified parents or
+  // children.
+  getLatestEnd: function() {
+    var latestEnd = this.get("end");
+    htrace.treeTraverseDepthFirstPre(this, htrace.getReifiedParents, 0,
+        function(span, depth) {
+          latestEnd = Math.max(latestEnd, span.get("end"));
+        });
+    htrace.treeTraverseDepthFirstPre(this, htrace.getReifiedChildren, 0,
+        function(span, depth) {
+          latestEnd = Math.max(latestEnd, span.get("end"));
+        });
+    return latestEnd;
+  },
 });
diff --git a/htrace-htraced/src/web/app/span_group_widget.js b/htrace-htraced/src/web/app/span_group_widget.js
new file mode 100644
index 0000000..e32c2db
--- /dev/null
+++ b/htrace-htraced/src/web/app/span_group_widget.js
@@ -0,0 +1,110 @@
+/*
+ * 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 || {};
+
+// Widget containing a group of trace spans displayed on the canvas.
+htrace.SpanGroupWidget = function(params) {
+  this.draw = function() {
+    this.ctx.save();
+    this.ctx.fillStyle="#ffffff";
+    this.ctx.fillRect(this.x0, this.y0, this.xF - this.x0, this.yF - this.y0);
+    this.ctx.strokeStyle="#aaaaaa";
+    this.ctx.beginPath();
+    this.ctx.moveTo(this.x0, this.y0);
+    this.ctx.lineTo(this.xF, this.y0);
+    this.ctx.stroke();
+    this.ctx.beginPath();
+    this.ctx.moveTo(this.x0, this.yF);
+    this.ctx.lineTo(this.xF, this.yF);
+    this.ctx.stroke();
+    this.ctx.restore();
+    return true;
+  };
+
+  this.createSpanWidget = function(node, indentLevel,
+      allowUpButton, allowDownButton) {
+    new htrace.SpanWidget({
+      manager: this.manager,
+      ctx: this.ctx,
+      span: node,
+      x0: this.x0,
+      xB: this.xB,
+      xD: this.xD,
+      xF: this.xF,
+      xT: this.childIndent * indentLevel,
+      y0: this.spanY,
+      yF: this.spanY + this.spanWidgetHeight,
+      allowUpButton: allowUpButton,
+      allowDownButton: allowDownButton,
+      begin: this.begin,
+      end: this.end
+    });
+    this.spanY += this.spanWidgetHeight;
+  }
+
+  this.handle = function(e) {
+    switch (e.type) {
+      case "draw":
+        this.draw();
+        return true;
+    }
+  }
+
+  for (var k in params) {
+    this[k]=params[k];
+  }
+  this.manager.register("draw", this);
+  this.spanY = this.y0 + 4;
+
+  // Figure out how much to indent each child's description text.
+  this.childIndent = Math.max(10, (this.xF - this.xD) / 50);
+
+  // Get the maximum depth of the parents tree to find out how far to indent.
+  var parentTreeHeight =
+      htrace.treeHeight(this.span, htrace.getReifiedParents);
+
+  console.log("parentTreeHeight = " + parentTreeHeight);
+  // Traverse the parents tree upwards.
+  var thisWidget = this;
+  htrace.treeTraverseDepthFirstPost(this.span, htrace.getReifiedParents, 0,
+      function(node, depth) {
+        if (depth > 0) {
+          thisWidget.createSpanWidget(node,
+              parentTreeHeight - depth, true, false);
+        }
+      });
+  thisWidget.createSpanWidget(this.span, parentTreeHeight, true, true);
+  // Traverse the children tree downwards.
+  htrace.treeTraverseDepthFirstPre(this.span, htrace.getReifiedChildren, 0,
+      function(node, depth) {
+        if (depth > 0) {
+          thisWidget.createSpanWidget(node,
+              parentTreeHeight + depth, false, true);
+        }
+      });
+  this.yF = this.spanY + 4;
+  console.log("SpanGroupWidget(this.span=" +
+      JSON.stringify(this.span.unparse()) +
+      ", x0=" + this.x0 + ", xB=" + this.xB +
+      ", xD=" + this.xD + ", xF=" + this.xF +
+      ", y0=" + this.y0 + ", yF=" + this.yF +
+      ")");
+  return this;
+};
diff --git a/htrace-htraced/src/web/app/span_widget.js b/htrace-htraced/src/web/app/span_widget.js
index f9333d6..0d18fef 100644
--- a/htrace-htraced/src/web/app/span_widget.js
+++ b/htrace-htraced/src/web/app/span_widget.js
@@ -21,50 +21,16 @@
 
 // Widget containing the trace span displayed on the canvas.
 htrace.SpanWidget = function(params) {
-  for (var k in params) {
-    this[k]=params[k];
-  }
-
-  this.selected = false;
-  this.widgetManagerFocused = false;
-  this.xSize = this.xF - this.x0;
-  this.ySize = this.yF - this.y0;
-  this.xDB = this.xD - this.xB;
-
-  var widgets = [];
-  this.upWidget = new htrace.TriangleButton({
-    ctx: this.ctx,
-    direction: "up",
-    x0: this.xB + 2,
-    xF: this.xB + (this.xDB / 2) - 2,
-    y0: this.y0 + 2,
-    yF: this.yF - 2,
-  });
-  widgets.push(this.upWidget);
-  this.downWidget = new htrace.TriangleButton({
-    ctx: this.ctx,
-    direction: "down",
-    x0: this.xB + (this.xDB / 2) + 2,
-    xF: this.xD - 2,
-    y0: this.y0 + 2,
-    yF: this.yF - 2,
-  });
-  widgets.push(this.downWidget);
-  this.widgetManager = new htrace.WidgetManager({
-    widgets: widgets,
-  });
-
   this.draw = function() {
     this.drawBackground();
     this.drawProcessId();
     this.drawDescription();
-    this.widgetManager.draw();
   };
 
   // Draw the background of this span widget.
   this.drawBackground = function() {
     this.ctx.save();
-    if (this.selected) {
+    if (this.span.get("selected")) {
       this.ctx.fillStyle="#ffccff";
     } else {
       this.ctx.fillStyle="#ffffff";
@@ -133,7 +99,9 @@
     // Draw description text
     this.ctx.fillStyle="#000000";
     this.ctx.font = (this.ySize - gapY) + "px sans-serif";
-    this.ctx.fillText(this.span.get('description'), this.xD, this.yF - gapY - 2);
+    this.ctx.fillText(this.span.get('description'),
+        this.xD + this.xT,
+        this.yF - gapY - 2);
 
     this.ctx.restore();
   };
@@ -145,42 +113,11 @@
         (this.end - this.begin));
   };
 
-  this.inBoundingBox = function(x, y) {
-    return ((x >= this.x0) && (x <= this.xF) && (y >= this.y0) && (y <= this.yF));
-  };
-
-  this.handleMouseDown = function(x, y) {
-    if (!this.inBoundingBox(x, y)) {
-      return false;
-    }
-    if (this.widgetManager.handleMouseDown(x, y)) {
-      this.widgetManagerFocused = true;
-      return true;
-    }
-    this.selected = !this.selected;
-    this.fillSpanDetailsView();
-    return true;
-  };
-
-  this.handleMouseUp = function(x, y) {
-    if (this.widgetManagerFocused) {
-      this.widgetManager.handleMouseUp(x, y);
-      this.widgetManagerFocused = false;
-    }
-  };
-
-  this.handleMouseMove = function(x, y) {
-    if (!this.widgetManagerFocused) {
-      return false;
-    }
-    return this.widgetManager.handleMouseUp(x, y);
-  };
-
   this.fillSpanDetailsView = function() {
     var info = {
       spanID: this.span.get("spanID"),
       begin: htrace.dateToString(parseInt(this.span.get("begin"), 10)),
-      end: htrace.dateToString(parseInt(this.span.get("end"), 10))
+      end: htrace.dateToString(parseInt(this.span.get("end"), 10)),
     };
     var explicitOrder = {
       spanId: -3,
@@ -189,6 +126,12 @@
     };
     keys = [];
     for(k in this.span.attributes) {
+      if (k == "reifiedChildren") {
+        continue;
+      }
+      if (k == "reifiedParents") {
+        continue;
+      }
       keys.push(k);
       if (info[k] == null) {
         info[k] = this.span.get(k);
@@ -225,5 +168,70 @@
     $("#spanDetails").html(h);
   };
 
+  this.handle = function(e) {
+    switch (e.type) {
+      case "mouseDown":
+        if (!htrace.inBoundingBox(e.x, e.y,
+              this.x0, this.xF, this.y0, this.yF)) {
+          return true;
+        }
+        this.manager.searchResultsView.applyToAllSpans(function(span) {
+            if (span.get("selected") == true) {
+              span.set("selected", false);
+            }
+          });
+        this.span.set("selected", true);
+        this.fillSpanDetailsView();
+        return true;
+      case "draw":
+        this.draw();
+        return true;
+    }
+  };
+
+  for (var k in params) {
+    this[k]=params[k];
+  }
+  this.xSize = this.xF - this.x0;
+  this.ySize = this.yF - this.y0;
+  this.xDB = this.xD - this.xB;
+  this.manager.register("draw", this);
+
+  var widget = this;
+  if ((this.span.get("reifiedParents") == null) && (this.allowUpButton)) {
+    new htrace.TriangleButton({
+      ctx: this.ctx,
+      manager: this.manager,
+      direction: "up",
+      x0: this.xB + 2,
+      xF: this.xB + (this.xDB / 2) - 2,
+      y0: this.y0 + 2,
+      yF: this.yF - 2,
+      callback: function() {
+        $.when(widget.span.reifyParents()).done(function (result) {
+          console.log("reifyParents: result was '" + result + "'");
+          widget.manager.searchResultsView.render();
+        });
+      },
+    });
+  }
+  if ((this.span.get("reifiedChildren") == null) && (this.allowDownButton)) {
+    new htrace.TriangleButton({
+      ctx: this.ctx,
+      manager: this.manager,
+      direction: "down",
+      x0: this.xB + (this.xDB / 2) + 2,
+      xF: this.xD - 2,
+      y0: this.y0 + 2,
+      yF: this.yF - 2,
+      callback: function() {
+        $.when(widget.span.reifyChildren()).done(function (result) {
+          console.log("reifyChildren: result was '" + result + "'");
+          widget.manager.searchResultsView.render();
+        });
+      },
+    });
+  }
+  this.manager.register("mouseDown", this);
   return this;
 };
diff --git a/htrace-htraced/src/web/app/time_cursor.js b/htrace-htraced/src/web/app/time_cursor.js
index 0060abb..1caaa9a 100644
--- a/htrace-htraced/src/web/app/time_cursor.js
+++ b/htrace-htraced/src/web/app/time_cursor.js
@@ -21,11 +21,6 @@
 
 // Draws a vertical bar selecting a time.
 htrace.TimeCursor = function(params) {
-  this.selectedTime = -1;
-  for (var k in params) {
-    this[k]=params[k];
-  }
-
   this.positionToTime = function(x) {
     if ((x < this.x0) || (x > this.xF)) {
       return -1;
@@ -56,19 +51,31 @@
     }
   };
 
-  this.handleMouseMove = function(x, y) {
-    if ((y >= this.y0) && (y <= this.yF) &&
-        (x >= this.x0) && (x <= this.xF)) {
-      this.selectedTime = this.positionToTime(x);
-      if (this.selectedTime < 0) {
-        $(this.el).val("");
-      } else {
-        $(this.el).val(htrace.dateToString(this.selectedTime));
-      }
-      return true;
+  this.handle = function(e) {
+    switch (e.type) {
+      case "mouseMove":
+        if (htrace.inBoundingBox(e.x, e.y,
+              this.x0, this.xF, this.y0, this.yF)) {
+          this.selectedTime = this.positionToTime(e.x);
+          if (this.selectedTime < 0) {
+            $(this.el).val("");
+          } else {
+            $(this.el).val(htrace.dateToString(this.selectedTime));
+          }
+          return true;
+        }
+        return true;
+      case "draw":
+        this.draw();
+        return true;
     }
-    return false;
   };
 
+  this.selectedTime = -1;
+  for (var k in params) {
+    this[k]=params[k];
+  }
+  this.manager.register("mouseMove", this);
+  this.manager.register("draw", this);
   return this;
 };
diff --git a/htrace-htraced/src/web/app/tree.js b/htrace-htraced/src/web/app/tree.js
new file mode 100644
index 0000000..046085c
--- /dev/null
+++ b/htrace-htraced/src/web/app/tree.js
@@ -0,0 +1,74 @@
+/*
+ * 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 || {};
+
+//
+// Get the height of a tree-- that is, the number of edges on the longest
+// downward path between the root and a leaf
+//
+htrace.treeHeight = function(node, getDescendants) {
+  var height = 0;
+  var descendants = getDescendants(node);
+  for (var i = 0; i < descendants.length; i++) {
+    height = Math.max(height,
+        1 + htrace.treeHeight(descendants[i], getDescendants));
+  }
+  return height;
+};
+
+//
+// Perform a depth-first, post-order traversal on the tree, invoking the
+// callback on every node with the node and depth as the arguments.
+//
+// Example:
+//     5
+//    / \
+//   3   4
+//  / \
+// 1   2
+//
+htrace.treeTraverseDepthFirstPost = function(node, getDescendants, depth, cb) {
+  var descendants = getDescendants(node);
+  for (var i = 0; i < descendants.length; i++) {
+    htrace.treeTraverseDepthFirstPost(descendants[i],
+        getDescendants, depth + 1, cb);
+  }
+  cb(node, depth);
+};
+
+//
+// Perform a depth-first, pre-order traversal on the tree, invoking the
+// callback on every node with the node and depth as the arguments.
+//
+// Example:
+//     1
+//    / \
+//   2   5
+//  / \
+// 3   4
+//
+htrace.treeTraverseDepthFirstPre = function(node, getDescendants, depth, cb) {
+  cb(node, depth);
+  var descendants = getDescendants(node);
+  for (var i = 0; i < descendants.length; i++) {
+    htrace.treeTraverseDepthFirstPre(descendants[i],
+        getDescendants, depth + 1, cb);
+  }
+};
diff --git a/htrace-htraced/src/web/app/triangle_button.js b/htrace-htraced/src/web/app/triangle_button.js
index 89f9514..f252476 100644
--- a/htrace-htraced/src/web/app/triangle_button.js
+++ b/htrace-htraced/src/web/app/triangle_button.js
@@ -60,44 +60,49 @@
     } else {
       console.log("TriangleButton: unknown direction " + this.direction);
     }
-    this.ctx.closePath(); 
+    this.ctx.closePath();
     this.ctx.fill();
     this.ctx.restore();
   };
 
-  this.inBoundingBox = function(x, y) {
-    return ((x >= this.x0) && (x <= this.xF) && (y >= this.y0) && (y <= this.yF));
-  }
-
-  this.handleMouseDown = function(x, y) {
-//    console.log("TriangleButton#handleMouseDown(x=" + x + ", y=" + y +
-//        ", x0=" + this.x0 + ", y0="+ this.y0 +
-//        ", xF=" + this.xF + ", yF=" + this.yF);
-    if (this.inBoundingBox(x,y)) {
-      this.selected = true;
-      return true;
+  this.handle = function(e) {
+    switch (e.type) {
+      case "mouseDown":
+        if (!htrace.inBoundingBox(e.x, e.y,
+              this.x0, this.xF, this.y0, this.yF)) {
+          return true;
+        }
+        this.manager.register("mouseUp", this);
+        this.manager.register("mouseMove", this);
+        this.manager.register("mouseOut", this);
+        this.selected = true;
+        return false;
+      case "mouseUp":
+        if (this.selected) {
+          this.callback();
+          this.selected = false;
+        }
+        this.manager.unregister("mouseUp", this);
+        this.manager.unregister("mouseMove", this);
+        this.manager.unregister("mouseOut", this);
+        return true;
+      case "mouseMove":
+        this.selected = htrace.inBoundingBox(e.x, e.y,
+                this.x0, this.xF, this.y0, this.yF);
+        return true;
+      case "mouseOut":
+        this.selected = false;
+        return true;
+      case "draw":
+        this.draw();
+        return true;
     }
-    return false;
-  }
-
-  this.handleMouseUp = function(x, y) {
-    if (this.selected) {
-      console.log("executing callback");
-    }
-    this.selected = false;
-  }
-
-  this.handleMouseMove = function(x, y) {
-    var selected = this.inBoundingBox(x,y);
-    if (this.selected != selected) {
-      this.selected = selected;
-      return true;
-    }
-    return false;
-  }
+  };
 
   for (var k in params) {
     this[k]=params[k];
   }
+  this.manager.register("mouseDown", this);
+  this.manager.register("draw", this);
   return this;
 };
diff --git a/htrace-htraced/src/web/app/widget_manager.js b/htrace-htraced/src/web/app/widget_manager.js
index 49202c5..5f393b0 100644
--- a/htrace-htraced/src/web/app/widget_manager.js
+++ b/htrace-htraced/src/web/app/widget_manager.js
@@ -19,44 +19,40 @@
 
 var htrace = htrace || {};
 
+// Check if a point is inside a bounding box.
+htrace.inBoundingBox = function(x, y, x0, xF, y0, yF) {
+    return ((x >= x0) && (x <= xF) && (y >= y0) && (y <= yF));
+  }
+
 // Manages a set of widgets on the canvas.
 // Buttons and sliders are both widgets.
 htrace.WidgetManager = function(params) {
-  this.widgets = [];
-  this.focusedWidget = null;
+  this.listeners = {
+    "mouseDown": [],
+    "mouseUp": [],
+    "mouseMove": [],
+    "mouseOut": [],
+    "draw": [],
+  };
 
-  this.handleMouseDown = function(x, y) {
-    if (this.focusedWidget != null) {
-      this.focusedWidget = null;
-    }
-    var numWidgets = this.widgets.length;
-    console.log("WidgetManager looking through " + numWidgets + " widgets.");
-    for (var i = 0; i < numWidgets; i++) {
-      if (this.widgets[i].handleMouseDown(x, y)) {
-        this.focusedWidget = this.widgets[i];
+  this.register = function(type, widget) {
+    this.listeners[type].push(widget);
+  }
+
+  this.unregister = function(type, widget) {
+    this.listeners[type] = _.without(this.listeners[type], widget);
+  }
+
+  this.handle = function(e) {
+    // Make a copy of the listeners, in case the handling functions change the
+    // array.
+    var listeners = this.listeners[e.type].slice();
+    var len = listeners.length;
+    for (var i = 0; i < len; i++) {
+      if (!listeners[i].handle(e)) {
         break;
       }
     }
-    return (this.focusedWidget != null);
-  };
-
-  this.handleMouseUp = function(x, y) {
-    if (this.focusedWidget != null) {
-      this.focusedWidget.handleMouseUp(x, y);
-      this.focusedWidget = null;
-    }
-  };
-
-  this.handleMouseMove = function(x, y) {
-    return this.focusedWidget != null ?
-      this.focusedWidget.handleMouseMove(x, y) : false;
-  };
-
-  this.draw = function() {
-    var numWidgets = this.widgets.length;
-    for (var i = 0; i < numWidgets; i++) {
-      this.widgets[i].draw();
-    }
   };
 
   for (var k in params) {
diff --git a/htrace-htraced/src/web/image/owl.png b/htrace-htraced/src/web/image/owl.png
new file mode 100644
index 0000000..be6fabd
--- /dev/null
+++ b/htrace-htraced/src/web/image/owl.png
Binary files differ
diff --git a/htrace-htraced/src/web/index.html b/htrace-htraced/src/web/index.html
index 66ef0dc..bd15c9a 100644
--- a/htrace-htraced/src/web/index.html
+++ b/htrace-htraced/src/web/index.html
@@ -61,6 +61,45 @@
         <div class="col-md-3" role="form">
           <div class="panel panel-default">
             <div class="panel-heading">
+              <h1 class="panel-title">Timeline</h1>
+            </div style="border: 1px solid #000000;">
+            <div class="panel-body">
+              <div class="form-horizontal">
+                <div class="form-group">
+                  <label class="col-sm-2 control-label">Begin</label>
+                  <div class="col-sm-10">
+                    <input type="text" class="form-control" id="begin" value="1970-01-01T00:00:00,000"/>
+                  </div>
+                </div>
+                <div class="form-group">
+                  <label class="col-sm-2 control-label">End</label>
+                  <div class="col-sm-10">
+                    <input type="text" class="form-control" id="end" value="1970-01-01T00:00:00,100"/>
+                  </div>
+                </div>
+                <div class="form-group">
+                  <label class="col-sm-2 control-label">Cur</label>
+                  <div class="col-sm-10">
+                    <input type="text" class="form-control" id="selectedTime" value=""/>
+                  </div>
+                </div>
+                <div class="form-horizontal">
+                  <button type="button" class="btn btn btn-warning"
+                      id="zoomButton">Zoom</button>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h1 class="panel-title">Span Details</h1>
+            </div style="border: 1px solid #000000;">
+            <div class="panel-body">
+              <div id="spanDetails" ></div>
+            </div>
+          </div>
+          <div class="panel panel-default">
+            <div class="panel-heading">
               <h1 class="panel-title">Search</h1>
             </div style="border: 1px solid #000000;">
             <div class="panel-body">
@@ -105,45 +144,6 @@
               </form>
             </div>
           </div>
-          <div class="panel panel-default">
-            <div class="panel-heading">
-              <h1 class="panel-title">Timeline</h1>
-            </div style="border: 1px solid #000000;">
-            <div class="panel-body">
-              <div class="form-horizontal">
-                <div class="form-group">
-                  <label class="col-sm-2 control-label">Begin</label>
-                  <div class="col-sm-10">
-                    <input type="text" class="form-control" id="begin" value="1970-01-01T00:00:00,000"/>
-                  </div>
-                </div>
-                <div class="form-group">
-                  <label class="col-sm-2 control-label">End</label>
-                  <div class="col-sm-10">
-                    <input type="text" class="form-control" id="end" value="1970-01-01T00:00:00,100"/>
-                  </div>
-                </div>
-                <div class="form-group">
-                  <label class="col-sm-2 control-label">Cur</label>
-                  <div class="col-sm-10">
-                    <input type="text" class="form-control" id="selectedTime" value=""/>
-                  </div>
-                </div>
-                <div class="form-horizontal">
-                  <button type="button" class="btn btn btn-warning"
-                      id="zoomButton">Zoom</button>
-                </div>
-              </div>
-            </div>
-          </div>
-          <div class="panel panel-default">
-            <div class="panel-heading">
-              <h1 class="panel-title">Span Details</h1>
-            </div style="border: 1px solid #000000;">
-            <div class="panel-body">
-              <div id="spanDetails" ></div>
-            </div>
-          </div>
         </div>
         <div class="col-md-9" id="resultsView">
           <div id="results">
@@ -200,6 +200,7 @@
     <script src="lib/moment-2.10.3.js" type="text/javascript"></script>
 
     <script src="app/string.js" type="text/javascript"></script>
+    <script src="app/tree.js" type="text/javascript"></script>
     <script src="app/time_cursor.js" type="text/javascript"></script>
 
     <script src="app/widget_manager.js" type="text/javascript"></script>
@@ -207,6 +208,7 @@
 
     <script src="app/span.js" type="text/javascript"></script>
 
+    <script src="app/span_group_widget.js" type="text/javascript"></script>
     <script src="app/span_widget.js" type="text/javascript"></script>
     <script src="app/search_results.js" type="text/javascript"></script>
     <script src="app/about_view.js" type="text/javascript"></script>