1/*
2 * Copyright (C) 2009 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31WebInspector.TimelineOverviewPane = function(categories)
32{
33    this.element = document.createElement("div");
34    this.element.id = "timeline-overview-panel";
35
36    this._categories = categories;
37    this._overviewSidebarElement = document.createElement("div");
38    this._overviewSidebarElement.id = "timeline-overview-sidebar";
39    this.element.appendChild(this._overviewSidebarElement);
40
41    var overviewTreeElement = document.createElement("ol");
42    overviewTreeElement.className = "sidebar-tree";
43    this._overviewSidebarElement.appendChild(overviewTreeElement);
44    var sidebarTree = new TreeOutline(overviewTreeElement);
45
46    var categoriesTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("TIMELINES"), {}, true);
47    categoriesTreeElement.expanded = true;
48    sidebarTree.appendChild(categoriesTreeElement);
49    for (var categoryName in this._categories) {
50        var category = this._categories[categoryName];
51        categoriesTreeElement.appendChild(new WebInspector.TimelineCategoryTreeElement(category, this._onCheckboxClicked.bind(this, category)));
52    }
53
54    this._overviewGrid = new WebInspector.TimelineGrid();
55    this._overviewGrid.element.id = "timeline-overview-grid";
56    this._overviewGrid.itemsGraphsElement.id = "timeline-overview-graphs";
57    this.element.appendChild(this._overviewGrid.element);
58
59    this._categoryGraphs = {};
60    var i = 0;
61    for (var category in this._categories) {
62        var categoryGraph = new WebInspector.TimelineCategoryGraph(this._categories[category], i++ % 2);
63        this._categoryGraphs[category] = categoryGraph;
64        this._overviewGrid.itemsGraphsElement.appendChild(categoryGraph.graphElement);
65    }
66    this._overviewGrid.setScrollAndDividerTop(0, 0);
67
68    this._overviewWindowElement = document.createElement("div");
69    this._overviewWindowElement.id = "timeline-overview-window";
70    this._overviewWindowElement.addEventListener("mousedown", this._dragWindow.bind(this), false);
71    this._overviewGrid.element.appendChild(this._overviewWindowElement);
72
73    this._leftResizeElement = document.createElement("div");
74    this._leftResizeElement.className = "timeline-window-resizer";
75    this._leftResizeElement.style.left = 0;
76    this._overviewGrid.element.appendChild(this._leftResizeElement);
77    this._leftResizeElement.addEventListener("mousedown", this._resizeWindow.bind(this, this._leftResizeElement), false);
78
79    this._rightResizeElement = document.createElement("div");
80    this._rightResizeElement.className = "timeline-window-resizer timeline-window-resizer-right";
81    this._rightResizeElement.style.right = 0;
82    this._overviewGrid.element.appendChild(this._rightResizeElement);
83    this._rightResizeElement.addEventListener("mousedown", this._resizeWindow.bind(this, this._rightResizeElement), false);
84
85    this._overviewCalculator = new WebInspector.TimelineOverviewCalculator();
86
87    var separatorElement = document.createElement("div");
88    separatorElement.id = "timeline-overview-separator";
89    this.element.appendChild(separatorElement);
90
91    this.windowLeft = 0.0;
92    this.windowRight = 1.0;
93}
94
95
96WebInspector.TimelineOverviewPane.prototype = {
97    _onCheckboxClicked: function (category, event) {
98        if (event.target.checked)
99            category.hidden = false;
100        else
101            category.hidden = true;
102        this._categoryGraphs[category.name].dimmed = !event.target.checked;
103        this.dispatchEventToListeners("filter changed");
104    },
105
106    update: function(records)
107    {
108        // Clear summary bars.
109        var timelines = {};
110        for (var category in this._categories) {
111            timelines[category] = [];
112            this._categoryGraphs[category].clearChunks();
113        }
114
115        function forAllRecords(recordsArray, callback)
116        {
117            if (!recordsArray)
118                return;
119            for (var i = 0; i < recordsArray.length; ++i) {
120                callback(recordsArray[i]);
121                forAllRecords(recordsArray[i].children, callback);
122            }
123        }
124
125        // Create sparse arrays with 101 cells each to fill with chunks for a given category.
126        this._overviewCalculator.reset();
127        forAllRecords(records, this._overviewCalculator.updateBoundaries.bind(this._overviewCalculator));
128
129        function markTimeline(record)
130        {
131            var percentages = this._overviewCalculator.computeBarGraphPercentages(record);
132
133            var end = Math.round(percentages.end);
134            var categoryName = record.category.name;
135            for (var j = Math.round(percentages.start); j <= end; ++j)
136                timelines[categoryName][j] = true;
137        }
138        forAllRecords(records, markTimeline.bind(this));
139
140        // Convert sparse arrays to continuous segments, render graphs for each.
141        for (var category in this._categories) {
142            var timeline = timelines[category];
143            window.timelineSaved = timeline;
144            var chunkStart = -1;
145            for (var j = 0; j < 101; ++j) {
146                if (timeline[j]) {
147                    if (chunkStart === -1)
148                        chunkStart = j;
149                } else {
150                    if (chunkStart !== -1) {
151                        this._categoryGraphs[category].addChunk(chunkStart, j);
152                        chunkStart = -1;
153                    }
154                }
155            }
156            if (chunkStart !== -1) {
157                this._categoryGraphs[category].addChunk(chunkStart, 100);
158                chunkStart = -1;
159            }
160        }
161        this._overviewGrid.updateDividers(true, this._overviewCalculator);
162    },
163
164    setSidebarWidth: function(width)
165    {
166        this._overviewSidebarElement.style.width = width + "px";
167    },
168
169    updateMainViewWidth: function(width)
170    {
171        this._overviewGrid.element.style.left = width + "px";
172    },
173
174    reset: function()
175    {
176        this.windowLeft = 0.0;
177        this.windowRight = 1.0;
178        this._setWindowPosition(0, this._overviewGrid.element.clientWidth);
179        this._overviewCalculator.reset();
180        this._overviewGrid.updateDividers(true, this._overviewCalculator);
181    },
182
183    _resizeWindow: function(resizeElement, event)
184    {
185        WebInspector.elementDragStart(resizeElement, this._windowResizeDragging.bind(this, resizeElement), this._endWindowDragging.bind(this), event, "col-resize");
186    },
187
188    _windowResizeDragging: function(resizeElement, event)
189    {
190        if (resizeElement === this._leftResizeElement)
191            this._resizeWindowLeft(event.pageX - this._overviewGrid.element.offsetLeft);
192        else
193            this._resizeWindowRight(event.pageX - this._overviewGrid.element.offsetLeft);
194        event.preventDefault();
195    },
196
197    _dragWindow: function(event)
198    {
199        WebInspector.elementDragStart(this._overviewWindowElement, this._windowDragging.bind(this, event.pageX,
200            this._leftResizeElement.offsetLeft, this._rightResizeElement.offsetLeft), this._endWindowDragging.bind(this), event, "ew-resize");
201    },
202
203    _windowDragging: function(startX, windowLeft, windowRight, event)
204    {
205        var delta = event.pageX - startX;
206        var start = windowLeft + delta;
207        var end = windowRight + delta;
208        var windowSize = windowRight - windowLeft;
209
210        if (start < 0) {
211            start = 0;
212            end = windowSize;
213        }
214
215        if (end > this._overviewGrid.element.clientWidth) {
216            end = this._overviewGrid.element.clientWidth;
217            start = end - windowSize;
218        }
219        this._setWindowPosition(start, end);
220
221        event.preventDefault();
222    },
223
224    _resizeWindowLeft: function(start)
225    {
226        // Glue to edge.
227        if (start < 10)
228            start = 0;
229        this._setWindowPosition(start, null);
230    },
231
232    _resizeWindowRight: function(end)
233    {
234        // Glue to edge.
235        if (end > this._overviewGrid.element.clientWidth - 10)
236            end = this._overviewGrid.element.clientWidth;
237        this._setWindowPosition(null, end);
238    },
239
240    _setWindowPosition: function(start, end)
241    {
242        if (typeof start === "number") {
243            if (start > this._rightResizeElement.offsetLeft - 4)
244                start = this._rightResizeElement.offsetLeft - 4;
245
246            this.windowLeft = start / this._overviewGrid.element.clientWidth;
247            this._leftResizeElement.style.left = this.windowLeft * 100 + "%";
248            this._overviewWindowElement.style.left = this.windowLeft * 100 + "%";
249        }
250        if (typeof end === "number") {
251            if (end < this._leftResizeElement.offsetLeft + 12)
252                end = this._leftResizeElement.offsetLeft + 12;
253
254            this.windowRight = end / this._overviewGrid.element.clientWidth;
255            this._rightResizeElement.style.left = this.windowRight * 100 + "%";
256        }
257        this._overviewWindowElement.style.width = (this.windowRight - this.windowLeft) * 100 + "%";
258        this.dispatchEventToListeners("window changed");
259    },
260
261    _endWindowDragging: function(event)
262    {
263        WebInspector.elementDragEnd(event);
264    }
265}
266
267WebInspector.TimelineOverviewPane.prototype.__proto__ = WebInspector.Object.prototype;
268
269
270WebInspector.TimelineOverviewCalculator = function()
271{
272    this._uiString = WebInspector.UIString.bind(WebInspector);
273}
274
275WebInspector.TimelineOverviewCalculator.prototype = {
276    computeBarGraphPercentages: function(record)
277    {
278        var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100;
279        var end = (record.endTime - this.minimumBoundary) / this.boundarySpan * 100;
280        return {start: start, end: end};
281    },
282
283    reset: function()
284    {
285        delete this.minimumBoundary;
286        delete this.maximumBoundary;
287    },
288
289    updateBoundaries: function(record)
290    {
291        if (typeof this.minimumBoundary === "undefined" || record.startTime < this.minimumBoundary) {
292            this.minimumBoundary = record.startTime;
293            return true;
294        }
295        if (typeof this.maximumBoundary === "undefined" || record.endTime > this.maximumBoundary) {
296            this.maximumBoundary = record.endTime;
297            return true;
298        }
299        return false;
300    },
301
302    get boundarySpan()
303    {
304        return this.maximumBoundary - this.minimumBoundary;
305    },
306
307    formatValue: function(value)
308    {
309        return Number.secondsToString(value, this._uiString);
310    }
311}
312
313
314WebInspector.TimelineCategoryTreeElement = function(category, onCheckboxClicked)
315{
316    this._category = category;
317    this._onCheckboxClicked = onCheckboxClicked;
318    // Pass an empty title, the title gets made later in onattach.
319    TreeElement.call(this, "", null, false);
320}
321
322WebInspector.TimelineCategoryTreeElement.prototype = {
323    onattach: function()
324    {
325        this.listItemElement.removeChildren();
326        this.listItemElement.addStyleClass("timeline-category-tree-item");
327        this.listItemElement.addStyleClass("timeline-category-" + this._category.name);
328
329        var label = document.createElement("label");
330
331        var checkElement = document.createElement("input");
332        checkElement.type = "checkbox";
333        checkElement.className = "timeline-category-checkbox";
334        checkElement.checked = true;
335        checkElement.addEventListener("click", this._onCheckboxClicked);
336        label.appendChild(checkElement);
337
338        var typeElement = document.createElement("span");
339        typeElement.className = "type";
340        typeElement.textContent = this._category.title;
341        label.appendChild(typeElement);
342
343        this.listItemElement.appendChild(label);
344    }
345}
346
347WebInspector.TimelineCategoryTreeElement.prototype.__proto__ = TreeElement.prototype;
348
349WebInspector.TimelineCategoryGraph = function(category, isEven)
350{
351    this._category = category;
352
353    this._graphElement = document.createElement("div");
354    this._graphElement.className = "timeline-graph-side timeline-overview-graph-side" + (isEven ? " even" : "");
355
356    this._barAreaElement = document.createElement("div");
357    this._barAreaElement.className = "timeline-graph-bar-area timeline-category-" + category.name;
358    this._graphElement.appendChild(this._barAreaElement);
359}
360
361WebInspector.TimelineCategoryGraph.prototype = {
362    get graphElement()
363    {
364        return this._graphElement;
365    },
366
367    addChunk: function(start, end)
368    {
369        var chunk = document.createElement("div");
370        chunk.className = "timeline-graph-bar";
371        this._barAreaElement.appendChild(chunk);
372        chunk.style.setProperty("left", start + "%");
373        chunk.style.setProperty("width", (end - start) + "%");
374    },
375
376    clearChunks: function()
377    {
378        this._barAreaElement.removeChildren();
379    },
380
381    set dimmed(dimmed)
382    {
383        if (dimmed)
384            this._barAreaElement.removeStyleClass("timeline-category-" + this._category.name);
385        else
386            this._barAreaElement.addStyleClass("timeline-category-" + this._category.name);
387    }
388}
389