| /* |
| * Copyright (C) 2014, 2015 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.TimelineRecordBar = class TimelineRecordBar extends WI.Object |
| { |
| constructor(delegate, records, renderMode) |
| { |
| super(); |
| |
| this._delegate = delegate || null; |
| |
| this._element = document.createElement("div"); |
| this._element.classList.add("timeline-record-bar"); |
| this._element[WI.TimelineRecordBar.ElementReferenceSymbol] = this; |
| this._element.addEventListener("click", this._handleClick.bind(this)); |
| |
| this.renderMode = renderMode; |
| this.records = records; |
| } |
| |
| static createCombinedBars(records, secondsPerPixel, graphDataSource, createBarCallback) |
| { |
| if (!records.length) |
| return; |
| |
| var startTime = graphDataSource.startTime; |
| var currentTime = graphDataSource.currentTime; |
| var endTime = graphDataSource.endTime; |
| |
| var visibleRecords = []; |
| var usesActiveStartTime = false; |
| var lastRecordType = null; |
| |
| // FIXME: Do a binary search for records that fall inside start and current time. |
| |
| for (var i = 0; i < records.length; ++i) { |
| var record = records[i]; |
| if (isNaN(record.startTime)) |
| continue; |
| |
| // If this bar is completely before the bounds of the graph, skip this record. |
| if (record.endTime < startTime) |
| continue; |
| |
| // If this record is completely after the current time or end time, break out now. |
| // Records are sorted, so all records after this will be beyond the current or end time too. |
| if (record.startTime > currentTime || record.startTime > endTime) |
| break; |
| |
| if (record.usesActiveStartTime) |
| usesActiveStartTime = true; |
| |
| // If one record uses active time the rest are assumed to use it. |
| console.assert(record.usesActiveStartTime === usesActiveStartTime); |
| |
| // Only a single record type is supported right now. |
| console.assert(!lastRecordType || record.type === lastRecordType); |
| |
| visibleRecords.push(record); |
| |
| lastRecordType = record.type; |
| } |
| |
| if (!visibleRecords.length) |
| return; |
| |
| if (visibleRecords.length === 1) { |
| createBarCallback(visibleRecords, WI.TimelineRecordBar.RenderMode.Normal); |
| return; |
| } |
| |
| function compareByActiveStartTime(a, b) |
| { |
| return a.activeStartTime - b.activeStartTime; |
| } |
| |
| var minimumDuration = secondsPerPixel * WI.TimelineRecordBar.MinimumWidthPixels; |
| var minimumMargin = secondsPerPixel * WI.TimelineRecordBar.MinimumMarginPixels; |
| |
| if (usesActiveStartTime) { |
| var inactiveStartTime = NaN; |
| var inactiveEndTime = NaN; |
| var inactiveRecords = []; |
| |
| for (var i = 0; i < visibleRecords.length; ++i) { |
| var record = visibleRecords[i]; |
| |
| // Check if the previous record is far enough away to create the inactive bar. |
| if (!isNaN(inactiveStartTime) && inactiveStartTime + Math.max(inactiveEndTime - inactiveStartTime, minimumDuration) + minimumMargin <= record.startTime) { |
| createBarCallback(inactiveRecords, WI.TimelineRecordBar.RenderMode.InactiveOnly); |
| inactiveRecords = []; |
| inactiveStartTime = NaN; |
| inactiveEndTime = NaN; |
| } |
| |
| // If this is a new bar, peg the start time. |
| if (isNaN(inactiveStartTime)) |
| inactiveStartTime = record.startTime; |
| |
| // Update the end time to be the maximum we encounter. inactiveEndTime might be NaN, so "|| 0" to prevent Math.max from returning NaN. |
| inactiveEndTime = Math.max(inactiveEndTime || 0, record.activeStartTime); |
| |
| inactiveRecords.push(record); |
| } |
| |
| // Create the inactive bar for the last record if needed. |
| if (!isNaN(inactiveStartTime)) |
| createBarCallback(inactiveRecords, WI.TimelineRecordBar.RenderMode.InactiveOnly); |
| |
| visibleRecords.sort(compareByActiveStartTime); |
| } |
| |
| var activeStartTime = NaN; |
| var activeEndTime = NaN; |
| var activeRecords = []; |
| |
| var startTimeProperty = usesActiveStartTime ? "activeStartTime" : "startTime"; |
| |
| for (var i = 0; i < visibleRecords.length; ++i) { |
| var record = visibleRecords[i]; |
| var startTime = record[startTimeProperty]; |
| |
| // Check if the previous record is far enough away to create the active bar. We also create it now if the current record has no active state time. |
| if (!isNaN(activeStartTime) && (activeStartTime + Math.max(activeEndTime - activeStartTime, minimumDuration) + minimumMargin <= startTime |
| || (isNaN(startTime) && !isNaN(activeEndTime)))) { |
| createBarCallback(activeRecords, WI.TimelineRecordBar.RenderMode.ActiveOnly); |
| activeRecords = []; |
| activeStartTime = NaN; |
| activeEndTime = NaN; |
| } |
| |
| if (isNaN(startTime)) |
| continue; |
| |
| // If this is a new bar, peg the start time. |
| if (isNaN(activeStartTime)) |
| activeStartTime = startTime; |
| |
| // Update the end time to be the maximum we encounter. activeEndTime might be NaN, so "|| 0" to prevent Math.max from returning NaN. |
| if (!isNaN(record.endTime)) |
| activeEndTime = Math.max(activeEndTime || 0, record.endTime); |
| |
| activeRecords.push(record); |
| } |
| |
| // Create the active bar for the last record if needed. |
| if (!isNaN(activeStartTime)) |
| createBarCallback(activeRecords, WI.TimelineRecordBar.RenderMode.ActiveOnly); |
| } |
| |
| static fromElement(element) |
| { |
| return element[WI.TimelineRecordBar.ElementReferenceSymbol] || null; |
| } |
| |
| // Public |
| |
| get element() |
| { |
| return this._element; |
| } |
| |
| get selected() |
| { |
| return this._element.classList.contains("selected"); |
| } |
| |
| set selected(selected) |
| { |
| this._element.classList.toggle("selected", !!selected); |
| } |
| |
| get renderMode() |
| { |
| return this._renderMode; |
| } |
| |
| set renderMode(renderMode) |
| { |
| this._renderMode = renderMode || WI.TimelineRecordBar.RenderMode.Normal; |
| } |
| |
| get records() |
| { |
| return this._records; |
| } |
| |
| set records(records) |
| { |
| let oldRecordType; |
| let oldRecordEventType; |
| let oldRecordUsesActiveStartTime = false; |
| if (this._records && this._records.length) { |
| let oldRecord = this._records[0]; |
| oldRecordType = oldRecord.type; |
| oldRecordEventType = oldRecord.eventType; |
| oldRecordUsesActiveStartTime = oldRecord.usesActiveStartTime; |
| } |
| |
| records = records || []; |
| |
| this._records = records; |
| |
| // Assume all records in the group are the same type. |
| if (this._records.length) { |
| let newRecord = this._records[0]; |
| if (newRecord.type !== oldRecordType) { |
| this._element.classList.remove(oldRecordType); |
| this._element.classList.add(newRecord.type); |
| } |
| // Although all records may not have the same event type, the first record is |
| // sufficient to determine the correct style for the record bar. |
| if (newRecord.eventType !== oldRecordEventType) { |
| this._element.classList.remove(oldRecordEventType); |
| this._element.classList.add(newRecord.eventType); |
| } |
| if (newRecord.usesActiveStartTime !== oldRecordUsesActiveStartTime) { |
| if (!this._delegate || !this._delegate.timelineRecordBarCustomChildren) |
| this._element.classList.toggle("has-inactive-segment", newRecord.usesActiveStartTime); |
| } |
| } else |
| this._element.classList.remove(oldRecordType, oldRecordEventType, "has-inactive-segment"); |
| } |
| |
| refresh(graphDataSource) |
| { |
| if (isNaN(graphDataSource.secondsPerPixel)) |
| return false; |
| |
| console.assert(graphDataSource.zeroTime); |
| console.assert(graphDataSource.startTime); |
| console.assert(graphDataSource.currentTime); |
| console.assert(graphDataSource.endTime); |
| console.assert(graphDataSource.secondsPerPixel); |
| |
| if (!this._records || !this._records.length) |
| return false; |
| |
| var firstRecord = this._records[0]; |
| var barStartTime = firstRecord.startTime; |
| |
| // If this bar has no time info, return early. |
| if (isNaN(barStartTime)) |
| return false; |
| |
| var graphStartTime = graphDataSource.startTime; |
| var graphEndTime = graphDataSource.endTime; |
| var graphCurrentTime = graphDataSource.currentTime; |
| |
| var barEndTime = this._records.reduce(function(previousValue, currentValue) { return Math.max(previousValue, currentValue.endTime); }, 0); |
| |
| // If this bar is completely after the current time, return early. |
| if (barStartTime > graphCurrentTime) |
| return false; |
| |
| // If this bar is completely before or after the bounds of the graph, return early. |
| if (barEndTime < graphStartTime || barStartTime > graphEndTime) |
| return false; |
| |
| var barUnfinished = isNaN(barEndTime) || barEndTime >= graphCurrentTime; |
| if (barUnfinished) |
| barEndTime = graphCurrentTime; |
| |
| var barDuration = barEndTime - barStartTime; |
| |
| var graphDuration = graphEndTime - graphStartTime; |
| |
| let newBarPosition = (barStartTime - graphStartTime) / graphDuration; |
| let property = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left"; |
| this._updateElementPosition(this._element, newBarPosition, property); |
| |
| var newBarWidth = ((barEndTime - graphStartTime) / graphDuration) - newBarPosition; |
| this._updateElementPosition(this._element, newBarWidth, "width"); |
| |
| if (this._delegate && this._delegate.timelineRecordBarCustomChildren) { |
| this._element.removeChildren(); |
| |
| this._element.classList.add("has-custom-children"); |
| this._element.classList.toggle("unfinished", barUnfinished); |
| |
| let children = this._delegate.timelineRecordBarCustomChildren(this); |
| for (let child of children) { |
| let childElement; |
| if (child.image) { |
| childElement = this._element.appendChild(document.createElement("img")); |
| childElement.src = child.image; |
| } else |
| childElement = this._element.appendChild(document.createElement("div")); |
| |
| childElement.classList.add(...child.classNames); |
| childElement.title = child.title; |
| this._updateElementPosition(childElement, (child.startTime - barStartTime) / barDuration, property); |
| |
| if (typeof child.endTime === "number") { |
| let childEndTime = !isNaN(child.endTime) ? child.endTime : barEndTime; |
| this._updateElementPosition(childElement, (childEndTime - child.startTime) / barDuration, "width"); |
| } |
| } |
| |
| return true; |
| } |
| |
| this._element.classList.remove("has-custom-children"); |
| |
| if (!this._activeBarElement && this._renderMode !== WI.TimelineRecordBar.RenderMode.InactiveOnly) { |
| this._activeBarElement = document.createElement("div"); |
| this._activeBarElement.classList.add("segment"); |
| } |
| |
| if (!firstRecord.usesActiveStartTime) { |
| this._element.classList.toggle("unfinished", barUnfinished); |
| |
| if (this._inactiveBarElement) |
| this._inactiveBarElement.remove(); |
| |
| if (this._renderMode === WI.TimelineRecordBar.RenderMode.InactiveOnly) { |
| if (this._activeBarElement) |
| this._activeBarElement.remove(); |
| |
| return false; |
| } |
| |
| // If this TimelineRecordBar is reused and had an inactive bar previously, clean it up. |
| this._activeBarElement.style.removeProperty(WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left"); |
| this._activeBarElement.style.removeProperty("width"); |
| |
| if (!this._activeBarElement.parentNode) |
| this._element.appendChild(this._activeBarElement); |
| |
| return true; |
| } |
| |
| // Find the earliest active start time for active only rendering, and the latest for the other modes. |
| // This matches the values that TimelineRecordBar.createCombinedBars uses when combining. |
| if (this._renderMode === WI.TimelineRecordBar.RenderMode.ActiveOnly) |
| var barActiveStartTime = this._records.reduce(function(previousValue, currentValue) { return Math.min(previousValue, currentValue.activeStartTime); }, Infinity); |
| else |
| var barActiveStartTime = this._records.reduce(function(previousValue, currentValue) { return Math.max(previousValue, currentValue.activeStartTime); }, 0); |
| |
| var inactiveUnfinished = isNaN(barActiveStartTime) || barActiveStartTime >= graphCurrentTime; |
| this._element.classList.toggle("unfinished", inactiveUnfinished); |
| |
| if (inactiveUnfinished) |
| barActiveStartTime = graphCurrentTime; |
| else if (this._renderMode === WI.TimelineRecordBar.RenderMode.Normal) { |
| // Hide the inactive segment when its duration is less than the minimum displayable size. |
| let minimumSegmentDuration = graphDataSource.secondsPerPixel * WI.TimelineRecordBar.MinimumWidthPixels; |
| if (barActiveStartTime - barStartTime < minimumSegmentDuration) { |
| barActiveStartTime = barStartTime; |
| if (this._inactiveBarElement) |
| this._inactiveBarElement.remove(); |
| } |
| } |
| |
| let showInactiveSegment = barActiveStartTime > barStartTime; |
| this._element.classList.toggle("has-inactive-segment", showInactiveSegment); |
| |
| let middlePercentage = (barActiveStartTime - barStartTime) / barDuration; |
| if (showInactiveSegment && this._renderMode !== WI.TimelineRecordBar.RenderMode.ActiveOnly) { |
| if (!this._inactiveBarElement) { |
| this._inactiveBarElement = document.createElement("div"); |
| this._inactiveBarElement.classList.add("segment"); |
| this._inactiveBarElement.classList.add("inactive"); |
| } |
| |
| let property = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "left" : "right"; |
| this._updateElementPosition(this._inactiveBarElement, 1 - middlePercentage, property); |
| this._updateElementPosition(this._inactiveBarElement, middlePercentage, "width"); |
| |
| if (!this._inactiveBarElement.parentNode) |
| this._element.insertBefore(this._inactiveBarElement, this._element.firstChild); |
| } |
| |
| if (!inactiveUnfinished && this._renderMode !== WI.TimelineRecordBar.RenderMode.InactiveOnly) { |
| let property = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left"; |
| this._updateElementPosition(this._activeBarElement, middlePercentage, property); |
| this._updateElementPosition(this._activeBarElement, 1 - middlePercentage, "width"); |
| |
| if (!this._activeBarElement.parentNode) |
| this._element.appendChild(this._activeBarElement); |
| } else if (this._activeBarElement) |
| this._activeBarElement.remove(); |
| |
| return true; |
| } |
| |
| // Private |
| |
| _updateElementPosition(element, newPosition, property) |
| { |
| newPosition *= 100; |
| |
| let newPositionAprox = Math.round(newPosition * 100); |
| let currentPositionAprox = Math.round(parseFloat(element.style[property]) * 100); |
| if (currentPositionAprox !== newPositionAprox) |
| element.style[property] = (newPositionAprox / 100) + "%"; |
| } |
| |
| _handleClick(event) |
| { |
| // Ensure that the container "click" listener added by `WI.TimelineOverview` isn't called. |
| event.__timelineRecordClickEventHandled = true; |
| |
| if (this._delegate && this._delegate.timelineRecordBarClicked) |
| this._delegate.timelineRecordBarClicked(this); |
| } |
| }; |
| |
| WI.TimelineRecordBar.ElementReferenceSymbol = Symbol("timeline-record-bar"); |
| |
| WI.TimelineRecordBar.MinimumWidthPixels = 4; |
| WI.TimelineRecordBar.MinimumMarginPixels = 1; |
| |
| WI.TimelineRecordBar.RenderMode = { |
| Normal: "timeline-record-bar-normal-render-mode", |
| InactiveOnly: "timeline-record-bar-inactive-only-render-mode", |
| ActiveOnly: "timeline-record-bar-active-only-render-mode" |
| }; |