text: // Copyright 2013 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  // TODO(viona): Write a README document/instructions.
  
  /** This view displays the event waterfall. */
  var WaterfallView = (function() {
    'use strict';
  
    // We inherit from DivView.
    var superClass = DivView;
  
    /**
     * @constructor
     */
    function WaterfallView() {
      assertFirstConstructorCall(WaterfallView);
  
      // Call superclass's constructor.
      superClass.call(this, WaterfallView.MAIN_BOX_ID);
  
      SourceTracker.getInstance().addSourceEntryObserver(this);
  
      // For adjusting the range of view.
      $(WaterfallView.SCALE_ID).addEventListener(
          'click', this.setStartEndTimes_.bind(this), true);
  
      $(WaterfallView.MAIN_BOX_ID).addEventListener(
          'mousewheel', this.scrollToZoom_.bind(this), true);
  
      $(WaterfallView.MAIN_BOX_ID).addEventListener(
          'scroll', this.scrollInfoTable_.bind(this), true);
  
      this.initializeSourceList_();
  
      window.onload = this.scrollInfoTable_();
    }
  
    WaterfallView.TAB_ID = 'tab-handle-waterfall';
    WaterfallView.TAB_NAME = 'Waterfall';
    WaterfallView.TAB_HASH = '#waterfall';
  
    // IDs for special HTML elements in events_waterfall_view.html.
    WaterfallView.MAIN_BOX_ID = 'waterfall-view-tab-content';
    WaterfallView.BAR_TABLE_ID = 'waterfall-view-time-bar-table';
    WaterfallView.BAR_TBODY_ID = 'waterfall-view-time-bar-tbody';
    WaterfallView.SCALE_ID = 'waterfall-view-adjust-to-window';
    WaterfallView.TIME_SCALE_HEADER_ID = 'waterfall-view-time-scale-labels';
    WaterfallView.TIME_RANGE_ID = 'waterfall-view-time-range-submit';
    WaterfallView.START_TIME_ID = 'waterfall-view-start-input';
    WaterfallView.END_TIME_ID = 'waterfall-view-end-input';
    WaterfallView.INFO_TABLE_ID = 'waterfall-view-information-table';
    WaterfallView.INFO_TBODY_ID = 'waterfall-view-information-tbody';
    WaterfallView.CONTROLS_ID = 'waterfall-view-controls';
    WaterfallView.ID_HEADER_ID = 'waterfall-view-id-header';
    WaterfallView.URL_HEADER_ID = 'waterfall-view-url-header';
  
    // The number of units mouse wheel deltas increase for each tick of the
    // wheel.
    var MOUSE_WHEEL_UNITS_PER_CLICK = 120;
  
    // Amount we zoom for one vertical tick of the mouse wheel, as a ratio.
    var MOUSE_WHEEL_ZOOM_RATE = 1.25;
    // Amount we scroll for one horizontal tick of the mouse wheel, in pixels.
    var MOUSE_WHEEL_SCROLL_RATE = MOUSE_WHEEL_UNITS_PER_CLICK;
  
    cr.addSingletonGetter(WaterfallView);
  
    WaterfallView.prototype = {
      // Inherit the superclass's methods.
      __proto__: superClass.prototype,
  
      /**
       * Creates new WaterfallRows for URL Requests when the sourceEntries are
       * updated if they do not already exist.
       * Updates pre-existing WaterfallRows that correspond to updated sources.
       */
      onSourceEntriesUpdated: function(sourceEntries) {
        if (this.startTime_ == null && sourceEntries.length > 0) {
          var logEntries = sourceEntries[0].getLogEntries();
          this.startTime_ = timeutil.convertTimeTicksToTime(logEntries[0].time);
          // Initial scale factor.
          this.scaleFactor_ = 0.1;
        }
        for (var i = 0; i < sourceEntries.length; ++i) {
          var sourceEntry = sourceEntries[i];
          var id = sourceEntry.getSourceId();
          if (sourceEntry.getSourceType() == EventSourceType.URL_REQUEST) {
            var row = this.sourceIdToRowMap_[id];
            if (!row) {
              var newRow = new WaterfallRow(this, sourceEntry);
              this.sourceIdToRowMap_[id] = newRow;
            } else {
              row.onSourceUpdated();
            }
          }
        }
        this.scrollInfoTable_();
        this.positionBarTable_();
        this.updateTimeScale_(this.scaleFactor_);
      },
  
      onAllSourceEntriesDeleted: function() {
        this.initializeSourceList_();
      },
  
      onLoadLogFinish: function(data) {
        return true;
      },
  
      getScaleFactor: function() {
        return this.scaleFactor_;
      },
  
      getStartTime: function() {
        return this.startTime_;
      },
  
      setGeometry: function(left, top, width, height) {
        superClass.prototype.setGeometry.call(this, left, top, width, height);
        this.scrollInfoTable_();
      },
  
      show: function(isVisible) {
        superClass.prototype.show.call(this, isVisible);
        if (isVisible) {
          this.scrollInfoTable_();
        }
      },
  
      /**
       * Initializes the list of source entries.  If source entries are already
       * being displayed, removes them all in the process.
       */
      initializeSourceList_: function() {
        this.sourceIdToRowMap_ = {};
        $(WaterfallView.BAR_TBODY_ID).innerHTML = '';
        $(WaterfallView.INFO_TBODY_ID).innerHTML = '';
        this.startTime_ = null;
        this.scaleFactor_ = null;
      },
  
      /**
       * Changes scroll position of the window such that horizontally, everything
       * within the specified range fits into the user's viewport.
       */
      adjustToWindow_: function(windowStart, windowEnd) {
        var waterfallLeft = $(WaterfallView.INFO_TABLE_ID).offsetWidth +
            $(WaterfallView.INFO_TABLE_ID).offsetLeft +
            $(WaterfallView.ID_HEADER_ID).offsetWidth;
        var maxWidth = $(WaterfallView.MAIN_BOX_ID).offsetWidth - waterfallLeft;
        var totalDuration = 0;
        if (windowEnd != -1) {
          totalDuration = windowEnd - windowStart;
        } else {
          for (var id in this.sourceIdToRowMap_) {
            var row = this.sourceIdToRowMap_[id];
            var rowDuration = row.getEndTime() - this.startTime_;
            if (totalDuration < rowDuration && !row.hide) {
              totalDuration = rowDuration;
            }
          }
        }
        if (totalDuration <= 0) {
          return;
        }
        this.scaleAll_(maxWidth / totalDuration);
        $(WaterfallView.MAIN_BOX_ID).scrollLeft =
            windowStart * this.scaleFactor_;
      },
  
      /** Updates the time tick indicators. */
      updateTimeScale_: function(scaleFactor) {
        var timePerTick = 1;
        var minTickDistance = 20;
  
        $(WaterfallView.TIME_SCALE_HEADER_ID).innerHTML = '';
  
        // Holder provides environment to prevent wrapping.
        var timeTickRow = addNode($(WaterfallView.TIME_SCALE_HEADER_ID), 'div');
        timeTickRow.classList.add('waterfall-view-time-scale-row');
  
        var availableWidth = $(WaterfallView.BAR_TBODY_ID).clientWidth;
        var tickDistance = scaleFactor * timePerTick;
  
        while (tickDistance < minTickDistance) {
          timePerTick = timePerTick * 10;
          tickDistance = scaleFactor * timePerTick;
        }
  
        var tickCount = availableWidth / tickDistance;
        for (var i = 0; i < tickCount; ++i) {
          var timeCell = addNode(timeTickRow, 'div');
          setNodeWidth(timeCell, tickDistance);
          timeCell.classList.add('waterfall-view-time-scale');
          timeCell.title = i * timePerTick + ' to ' +
             (i + 1) * timePerTick + ' ms';
          // Red marker for every 5th bar.
          if (i % 5 == 0) {
            timeCell.classList.add('waterfall-view-time-scale-special');
          }
        }
      },
  
      /**
       * Scales all existing rows by scaleFactor.
       */
      scaleAll_: function(scaleFactor) {
        this.scaleFactor_ = scaleFactor;
        for (var id in this.sourceIdToRowMap_) {
          var row = this.sourceIdToRowMap_[id];
          row.updateRow();
        }
        this.updateTimeScale_(scaleFactor);
      },
  
      scrollToZoom_: function(event) {
        // To use scrolling to control zoom, hold down the alt key and scroll.
        if ('wheelDelta' in event && event.altKey) {
          event.preventDefault();
          var zoomFactor = Math.pow(MOUSE_WHEEL_ZOOM_RATE,
               event.wheelDeltaY / MOUSE_WHEEL_UNITS_PER_CLICK);
  
          var waterfallLeft = $(WaterfallView.ID_HEADER_ID).offsetWidth +
              $(WaterfallView.URL_HEADER_ID).offsetWidth;
          var oldCursorPosition = event.pageX +
              $(WaterfallView.MAIN_BOX_ID).scrollLeft;
          var oldCursorPositionInTable = oldCursorPosition - waterfallLeft;
  
          this.scaleAll_(this.scaleFactor_ * zoomFactor);
  
          // Shifts the view when scrolling. newScroll could be less than 0 or
          // more than the maximum scroll position, but both cases are handled
          // by the inbuilt scrollLeft implementation.
          var newScroll =
              oldCursorPositionInTable * zoomFactor - event.pageX + waterfallLeft;
          $(WaterfallView.MAIN_BOX_ID).scrollLeft = newScroll;
        }
      },
  
      /**
       * Positions the bar table such that it is in line with the right edge of
       * the info table.
       */
      positionBarTable_: function() {
        var offsetLeft = $(WaterfallView.INFO_TABLE_ID).offsetWidth +
            $(WaterfallView.INFO_TABLE_ID).offsetLeft;
        $(WaterfallView.BAR_TABLE_ID).style.left = offsetLeft + 'px';
      },
  
      /**
       * Moves the info table when the page is scrolled vertically, ensuring that
       * the correct information is displayed on the page, and that no elements
       * are blocked unnecessarily.
       */
      scrollInfoTable_: function(event) {
        $(WaterfallView.INFO_TABLE_ID).style.top =
            $(WaterfallView.MAIN_BOX_ID).offsetTop +
            $(WaterfallView.BAR_TABLE_ID).offsetTop -
            $(WaterfallView.MAIN_BOX_ID).scrollTop + 'px';
  
        if ($(WaterfallView.INFO_TABLE_ID).offsetHeight >
            $(WaterfallView.MAIN_BOX_ID).clientHeight) {
          var scroll = $(WaterfallView.MAIN_BOX_ID).scrollTop;
          var bottomClip =
              $(WaterfallView.MAIN_BOX_ID).clientHeight -
              $(WaterfallView.BAR_TABLE_ID).offsetTop +
              $(WaterfallView.MAIN_BOX_ID).scrollTop;
          // Clips the information table such that it does not cover the scroll
          // bars or the controls bar.
          $(WaterfallView.INFO_TABLE_ID).style.clip = 'rect(' + scroll +
              'px auto ' + bottomClip + 'px auto)';
        }
      },
  
      /** Parses user input, then calls adjustToWindow to shift that into view. */
      setStartEndTimes_: function() {
        var windowStart = parseInt($(WaterfallView.START_TIME_ID).value);
        var windowEnd = parseInt($(WaterfallView.END_TIME_ID).value);
        if ($(WaterfallView.END_TIME_ID).value == '') {
          windowEnd = -1;
        }
        if ($(WaterfallView.START_TIME_ID).value == '') {
          windowStart = 0;
        }
        this.adjustToWindow_(windowStart, windowEnd);
      },
  
  
    };
  
    return WaterfallView;
  })();
  