blob: df54682732bdc3196d709593f61a60b672db9b85 [file] [log] [blame]
<!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="/model/model.html">
<script>
'use strict';
/**
* @fileoverview Code for the viewport.
*/
tr.exportTo('tr.c', function() {
var EventRegistry = tr.model.EventRegistry;
var RequestSelectionChangeEvent = tr.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 tr.b.Range();
this.length_ = 0;
this.guid_ = tr.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('Unrecognized 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 = tr.b.dictionaryKeys(eventsByBaseType)[0];
if (this.length === 1) {
var tmp = EventRegistry.getUserFriendlySingularName(eventTypeName);
return this[0].userFriendlyName;
}
var numEventTypes = tr.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 tr.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>