| <!DOCTYPE html> |
| <!-- |
| Copyright (c) 2012 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. |
| --> |
| |
| <link rel="import" href="/base/events.html"> |
| <link rel="import" href="/base/guid.html"> |
| <link rel="import" href="/base/range.html"> |
| <link rel="import" href="/base/iteration_helpers.html"> |
| <link rel="import" href="/core/trace_model/trace_model.html"> |
| |
| <script> |
| 'use strict'; |
| |
| /** |
| * @fileoverview Code for the viewport. |
| */ |
| tv.exportTo('tv.c', function() { |
| |
| var EventRegistry = tv.c.trace_model.EventRegistry; |
| |
| var RequestSelectionChangeEvent = tv.b.Event.bind( |
| undefined, 'requestSelectionChange', true, false); |
| |
| /** |
| * Represents a selection within a and its associated set of tracks. |
| * @constructor |
| */ |
| function Selection(opt_events) { |
| // sunburst_zoom_level is used by sunburst chart to remember |
| // zoom level across selection changes. |
| // TODO(gholap): get rid of this eventually when |
| // selections support frames. |
| this.sunburst_zoom_level = undefined; |
| |
| this.bounds_dirty_ = true; |
| this.bounds_ = new tv.b.Range(); |
| this.length_ = 0; |
| this.guid_ = tv.b.GUID.allocate(); |
| this.pushed_guids_ = {}; |
| |
| if (opt_events) { |
| if (opt_events instanceof Array) { |
| for (var i = 0; i < opt_events.length; i++) |
| this.push(opt_events[i]); |
| } else { |
| this.push(opt_events); |
| } |
| } |
| } |
| Selection.prototype = { |
| __proto__: Object.prototype, |
| |
| get bounds() { |
| if (this.bounds_dirty_) { |
| this.bounds_.reset(); |
| for (var i = 0; i < this.length_; i++) |
| this[i].addBoundsToRange(this.bounds_); |
| this.bounds_dirty_ = false; |
| } |
| return this.bounds_; |
| }, |
| |
| get duration() { |
| if (this.bounds_.isEmpty) |
| return 0; |
| return this.bounds_.max - this.bounds_.min; |
| }, |
| |
| get length() { |
| return this.length_; |
| }, |
| |
| get guid() { |
| return this.guid_; |
| }, |
| |
| clear: function() { |
| for (var i = 0; i < this.length_; ++i) |
| delete this[i]; |
| this.length_ = 0; |
| this.bounds_dirty_ = true; |
| }, |
| |
| // push pushes only unique events. |
| // If an event has been already pushed, do nothing. |
| push: function(event) { |
| if (event.guid == undefined) |
| throw new Error('Event must have a GUID'); |
| |
| if (this.contains(event)) |
| return event; |
| |
| this.pushed_guids_[event.guid] = true; |
| this[this.length_++] = event; |
| this.bounds_dirty_ = true; |
| return event; |
| }, |
| |
| contains: function(event) { |
| return this.pushed_guids_[event.guid]; |
| }, |
| |
| addSelection: function(selection) { |
| for (var i = 0; i < selection.length; i++) |
| this.push(selection[i]); |
| }, |
| |
| subSelection: function(index, count) { |
| count = count || 1; |
| |
| var selection = new Selection(); |
| selection.bounds_dirty_ = true; |
| if (index < 0 || index + count > this.length_) |
| throw new Error('Index out of bounds'); |
| |
| for (var i = index; i < index + count; i++) |
| selection.push(this[i]); |
| |
| return selection; |
| }, |
| |
| equals: function(that) { |
| if (this.length !== that.length) |
| return false; |
| for (var i = 0; i < this.length; i++) { |
| var event = this[i]; |
| if (that.pushed_guids_[event.guid] === undefined) |
| return false; |
| } |
| return true; |
| }, |
| |
| getEventsOrganizedByBaseType: function(opt_pruneEmpty) { |
| var events = {}; |
| var allTypeInfos = EventRegistry.getAllRegisteredTypeInfos(); |
| allTypeInfos.forEach(function(eventTypeInfo) { |
| events[eventTypeInfo.metadata.name] = new Selection(); |
| if (this.sunburst_zoom_level !== undefined) |
| events[eventTypeInfo.metadata.name].sunburst_zoom_level = |
| this.sunburst_zoom_level; |
| }, this); |
| |
| this.forEach(function(event, i) { |
| var maxEventIndex = -1; |
| var maxEventTypeInfo = undefined; |
| allTypeInfos.forEach(function(eventTypeInfo, eventIndex) { |
| if (!(event instanceof eventTypeInfo.constructor)) |
| return; |
| if (eventIndex > maxEventIndex) { |
| maxEventIndex = eventIndex; |
| maxEventTypeInfo = eventTypeInfo; |
| } |
| }); |
| if (maxEventIndex == -1) { |
| console.log(event); |
| throw new Error('Unrecgonized event type'); |
| } |
| events[maxEventTypeInfo.metadata.name].push(event); |
| }); |
| if (opt_pruneEmpty) { |
| var prunedEvents = {}; |
| for (var eventType in events) { |
| if (events[eventType].length > 0) |
| prunedEvents[eventType] = events[eventType]; |
| } |
| return prunedEvents; |
| } else { |
| return events; |
| } |
| }, |
| |
| getEventsOrganizedByTitle: function() { |
| var eventsByTitle = {}; |
| for (var i = 0; i < this.length; i++) { |
| var event = this[i]; |
| if (event.title === undefined) |
| throw new Error('An event didn\'t have a title!'); |
| if (eventsByTitle[event.title] == undefined) { |
| eventsByTitle[event.title] = []; |
| } |
| eventsByTitle[event.title].push(event); |
| } |
| return eventsByTitle; |
| }, |
| |
| enumEventsOfType: function(type, func) { |
| for (var i = 0; i < this.length_; i++) |
| if (this[i] instanceof type) |
| func(this[i]); |
| }, |
| |
| get userFriendlyName() { |
| if (this.length === 0) { |
| throw new Error('Empty selection'); |
| } |
| |
| var eventsByBaseType = this.getEventsOrganizedByBaseType(true); |
| var eventTypeName = tv.b.dictionaryKeys(eventsByBaseType)[0]; |
| |
| if (this.length === 1) { |
| var tmp = EventRegistry.getUserFriendlySingularName(eventTypeName); |
| return this[0].userFriendlyName; |
| } |
| |
| var numEventTypes = tv.b.dictionaryLength(eventsByBaseType); |
| if (numEventTypes !== 1) { |
| return this.length + ' events of various types'; |
| } |
| |
| var tmp = EventRegistry.getUserFriendlyPluralName(eventTypeName); |
| return this.length + ' ' + tmp; |
| }, |
| |
| /** |
| * Helper for selection previous or next. |
| * @param {boolean} offset If positive, select one forward (next). |
| * Else, select previous. |
| * |
| * @param {TimelineViewport} viewport The viewport to use to determine what |
| * is near to the current selection. |
| * |
| * @return {boolean} true if current selection changed. |
| */ |
| getShiftedSelection: function(viewport, offset) { |
| var newSelection = new Selection(); |
| for (var i = 0; i < this.length_; i++) { |
| var event = this[i]; |
| |
| // If this is a flow event, then move to its slice based on the |
| // offset direction. |
| if (event instanceof tv.c.trace_model.FlowEvent) { |
| if (offset > 0) { |
| newSelection.push(event.endSlice); |
| } else if (offset < 0) { |
| newSelection.push(event.startSlice); |
| } else { |
| /* Do nothing. Zero offsets don't do anything. */ |
| } |
| continue; |
| } |
| |
| var track = viewport.trackForEvent(event); |
| track.addEventNearToProvidedEventToSelection( |
| event, offset, newSelection); |
| } |
| |
| if (newSelection.length == 0) |
| return undefined; |
| return newSelection; |
| }, |
| |
| forEach: function(fn, opt_this) { |
| for (var i = 0; i < this.length; i++) |
| fn.call(opt_this, this[i], i); |
| }, |
| |
| map: function(fn, opt_this) { |
| var res = []; |
| for (var i = 0; i < this.length; i++) |
| res.push(fn.call(opt_this, this[i], i)); |
| return res; |
| }, |
| |
| every: function(fn, opt_this) { |
| for (var i = 0; i < this.length; i++) |
| if (!fn.call(opt_this, this[i], i)) |
| return false; |
| return true; |
| }, |
| |
| some: function(fn, opt_this) { |
| for (var i = 0; i < this.length; i++) |
| if (fn.call(opt_this, this[i], i)) |
| return true; |
| return false; |
| } |
| }; |
| |
| return { |
| Selection: Selection, |
| RequestSelectionChangeEvent: RequestSelectionChangeEvent |
| }; |
| }); |
| </script> |