[tools][system-analyzer] Timeline-track filter by time event
This CL adds the functionality to filter log events falling into the time range specified by the user via mouse events on timeline tracks. The log event selections on panels updated based on the selected time range. Bug: v8:10644 Change-Id: Iaf53896fd5c43cefea6d4c40bab5fcb136494b5f Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2351670 Commit-Queue: Zeynep Cankara <zcankara@google.com> Reviewed-by: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/master@{#69375}
This commit is contained in:
parent
833662c74a
commit
9c8ebcbbe2
@ -3,48 +3,50 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
class State {
|
||||
#timeSelection = {start: 0, end: Infinity};
|
||||
#timeSelection = { start: 0, end: Infinity };
|
||||
#map;
|
||||
#ic;
|
||||
#selectedMapLogEvents;
|
||||
#selectedIcLogEvents;
|
||||
#nofChunks;
|
||||
#chunks;
|
||||
#icTimeline;
|
||||
#mapTimeline;
|
||||
#minStartTime = Number.POSITIVE_INFINITY;
|
||||
#maxEndTime = Number.NEGATIVE_INFINITY;
|
||||
get minStartTime(){
|
||||
get minStartTime() {
|
||||
return this.#minStartTime;
|
||||
}
|
||||
get maxEndTime(){
|
||||
get maxEndTime() {
|
||||
return this.#maxEndTime;
|
||||
}
|
||||
#updateTimeRange(timeline){
|
||||
#updateTimeRange(timeline) {
|
||||
this.#minStartTime = Math.min(this.#minStartTime, timeline.startTime);
|
||||
this.#maxEndTime = Math.max(this.#maxEndTime, timeline.endTime);
|
||||
}
|
||||
get mapTimeline(){
|
||||
get mapTimeline() {
|
||||
return this.#mapTimeline;
|
||||
}
|
||||
set mapTimeline(timeline){
|
||||
set mapTimeline(timeline) {
|
||||
this.#updateTimeRange(timeline);
|
||||
timeline.startTime = this.#minStartTime;
|
||||
timeline.endTime = this.#maxEndTime;
|
||||
this.#mapTimeline = timeline;
|
||||
}
|
||||
set icTimeline(timeline){
|
||||
set icTimeline(timeline) {
|
||||
this.#updateTimeRange(timeline);
|
||||
timeline.startTime = this.#minStartTime;
|
||||
timeline.endTime = this.#maxEndTime;
|
||||
this.#icTimeline = timeline;
|
||||
}
|
||||
get icTimeline(){
|
||||
get icTimeline() {
|
||||
return this.#icTimeline;
|
||||
}
|
||||
set chunks(value){
|
||||
set chunks(value) {
|
||||
//TODO(zcankara) split up between maps and ics, and every timeline track
|
||||
this.#chunks = value;
|
||||
}
|
||||
get chunks(){
|
||||
get chunks() {
|
||||
//TODO(zcankara) split up between maps and ics, and every timeline track
|
||||
return this.#chunks;
|
||||
}
|
||||
@ -60,7 +62,7 @@ class State {
|
||||
}
|
||||
set map(value) {
|
||||
//TODO(zcankara) rename as selectedMapEvents, array of selected events
|
||||
if(!value) return;
|
||||
if (!value) return;
|
||||
this.#map = value;
|
||||
}
|
||||
get ic() {
|
||||
@ -69,9 +71,23 @@ class State {
|
||||
}
|
||||
set ic(value) {
|
||||
//TODO(zcankara) rename selectedIcEvents, array of selected events
|
||||
if(!value) return;
|
||||
if (!value) return;
|
||||
this.#ic = value;
|
||||
}
|
||||
get selectedMapLogEvents() {
|
||||
return this.#selectedMapLogEvents;
|
||||
}
|
||||
set selectedMapLogEvents(value) {
|
||||
if (!value) return;
|
||||
this.#selectedMapLogEvents = value;
|
||||
}
|
||||
get selectedIcLogEvents() {
|
||||
return this.#selectedIcLogEvents;
|
||||
}
|
||||
set selectedIcLogEvents(value) {
|
||||
if (!value) return;
|
||||
this.#selectedIcLogEvents = value;
|
||||
}
|
||||
get timeSelection() {
|
||||
return this.#timeSelection;
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
class SelectionEvent extends CustomEvent {
|
||||
constructor(entries){
|
||||
super('showentries', {bubbles: true, composed: true});
|
||||
if(!Array.isArray(entries) || entries.length == 0){
|
||||
constructor(entries) {
|
||||
super('showentries', { bubbles: true, composed: true });
|
||||
if (!Array.isArray(entries) || entries.length == 0) {
|
||||
throw new Error('No valid entries selected!')
|
||||
}
|
||||
this.entries = entries;
|
||||
@ -14,10 +14,19 @@ class SelectionEvent extends CustomEvent {
|
||||
}
|
||||
|
||||
class SelectEvent extends CustomEvent {
|
||||
constructor(entry){
|
||||
super('showentrydetail', {bubbles: true, composed: true});
|
||||
constructor(entry) {
|
||||
super('showentrydetail', { bubbles: true, composed: true });
|
||||
this.entry = entry;
|
||||
}
|
||||
}
|
||||
|
||||
export {SelectionEvent, SelectEvent};
|
||||
class SelectTimeEvent extends CustomEvent {
|
||||
static name = 'timerangeselect';
|
||||
constructor(start, end) {
|
||||
super(SelectTimeEvent.name, { bubbles: true, composed: true });
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
}
|
||||
|
||||
export { SelectionEvent, SelectEvent, SelectTimeEvent };
|
||||
|
@ -2,199 +2,197 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import {Group} from './ic-model.mjs';
|
||||
import { Group } from './ic-model.mjs';
|
||||
import CustomIcProcessor from "./ic-processor.mjs";
|
||||
import {SelectEvent} from './events.mjs';
|
||||
import {defineCustomElement, V8CustomElement} from './helper.mjs';
|
||||
import { SelectEvent, SelectTimeEvent } from './events.mjs';
|
||||
import { defineCustomElement, V8CustomElement } from './helper.mjs';
|
||||
|
||||
defineCustomElement('ic-panel', (templateText) =>
|
||||
class ICPanel extends V8CustomElement {
|
||||
//TODO(zcankara) Entries never set
|
||||
#entries;
|
||||
#filteredEntries;
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.groupKey.addEventListener(
|
||||
//TODO(zcankara) Entries never set
|
||||
#entries;
|
||||
#filteredEntries;
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.groupKey.addEventListener(
|
||||
'change', e => this.updateTable(e));
|
||||
this.$('#filterICTimeBtn').addEventListener(
|
||||
'click', e => this.handleICTimeFilter(e));
|
||||
}
|
||||
|
||||
get entries(){
|
||||
return this.#entries;
|
||||
}
|
||||
|
||||
get groupKey() {
|
||||
return this.$('#group-key');
|
||||
}
|
||||
|
||||
get table() {
|
||||
return this.$('#table');
|
||||
}
|
||||
|
||||
get tableBody() {
|
||||
return this.$('#table-body');
|
||||
}
|
||||
|
||||
get count() {
|
||||
return this.$('#count');
|
||||
}
|
||||
|
||||
get spanSelectAll(){
|
||||
return this.querySelectorAll("span");
|
||||
}
|
||||
|
||||
set filteredEntries(value){
|
||||
this.#filteredEntries = value;
|
||||
this.updateTable();
|
||||
}
|
||||
|
||||
get filteredEntries(){
|
||||
return this.#filteredEntries;
|
||||
}
|
||||
|
||||
updateTable(event) {
|
||||
let select = this.groupKey;
|
||||
let key = select.options[select.selectedIndex].text;
|
||||
let tableBody = this.tableBody;
|
||||
this.removeAllChildren(tableBody);
|
||||
let groups = Group.groupBy(this.filteredEntries, key, true);
|
||||
this.render(groups, tableBody);
|
||||
}
|
||||
|
||||
escapeHtml(unsafe) {
|
||||
if (!unsafe) return "";
|
||||
return unsafe.toString()
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
processValue(unsafe) {
|
||||
if (!unsafe) return "";
|
||||
if (!unsafe.startsWith("http")) return this.escapeHtml(unsafe);
|
||||
let a = document.createElement("a");
|
||||
a.href = unsafe;
|
||||
a.textContent = unsafe;
|
||||
return a;
|
||||
}
|
||||
|
||||
td(tr, content, className) {
|
||||
let node = document.createElement("td");
|
||||
if (typeof content == "object") {
|
||||
node.appendChild(content);
|
||||
} else {
|
||||
node.innerHTML = content;
|
||||
this.$('#filterICTimeBtn').addEventListener(
|
||||
'click', e => this.handleICTimeFilter(e));
|
||||
}
|
||||
node.className = className;
|
||||
tr.appendChild(node);
|
||||
return node
|
||||
}
|
||||
|
||||
handleMapClick(e){
|
||||
this.dispatchEvent(new SelectEvent(e.target.parentNode.entry));
|
||||
}
|
||||
get entries() {
|
||||
return this.#entries;
|
||||
}
|
||||
|
||||
handleFilePositionClick(e){
|
||||
this.dispatchEvent(new SelectEvent(e.target.parentNode.entry.key));
|
||||
}
|
||||
get groupKey() {
|
||||
return this.$('#group-key');
|
||||
}
|
||||
|
||||
render(entries, parent) {
|
||||
let fragment = document.createDocumentFragment();
|
||||
let max = Math.min(1000, entries.length)
|
||||
for (let i = 0; i < max; i++) {
|
||||
let entry = entries[i];
|
||||
let tr = document.createElement("tr");
|
||||
tr.entry = entry;
|
||||
//TODO(zcankara) Create one bound method and use it everywhere
|
||||
if (entry.property === "map") {
|
||||
tr.addEventListener('click', e => this.handleMapClick(e));
|
||||
} else if (entry.property == "filePosition") {
|
||||
tr.addEventListener('click',
|
||||
e => this.handleFilePositionClick(e));
|
||||
get table() {
|
||||
return this.$('#table');
|
||||
}
|
||||
|
||||
get tableBody() {
|
||||
return this.$('#table-body');
|
||||
}
|
||||
|
||||
get count() {
|
||||
return this.$('#count');
|
||||
}
|
||||
|
||||
get spanSelectAll() {
|
||||
return this.querySelectorAll("span");
|
||||
}
|
||||
|
||||
set filteredEntries(value) {
|
||||
this.#filteredEntries = value;
|
||||
this.updateTable();
|
||||
}
|
||||
|
||||
get filteredEntries() {
|
||||
return this.#filteredEntries;
|
||||
}
|
||||
|
||||
updateTable(event) {
|
||||
let select = this.groupKey;
|
||||
let key = select.options[select.selectedIndex].text;
|
||||
let tableBody = this.tableBody;
|
||||
this.removeAllChildren(tableBody);
|
||||
let groups = Group.groupBy(this.filteredEntries, key, true);
|
||||
this.render(groups, tableBody);
|
||||
}
|
||||
|
||||
escapeHtml(unsafe) {
|
||||
if (!unsafe) return "";
|
||||
return unsafe.toString()
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
processValue(unsafe) {
|
||||
if (!unsafe) return "";
|
||||
if (!unsafe.startsWith("http")) return this.escapeHtml(unsafe);
|
||||
let a = document.createElement("a");
|
||||
a.href = unsafe;
|
||||
a.textContent = unsafe;
|
||||
return a;
|
||||
}
|
||||
|
||||
td(tr, content, className) {
|
||||
let node = document.createElement("td");
|
||||
if (typeof content == "object") {
|
||||
node.appendChild(content);
|
||||
} else {
|
||||
node.innerHTML = content;
|
||||
}
|
||||
let details = this.td(tr,'<span>ℹ</a>', 'details');
|
||||
//TODO(zcankara) don't keep the whole function context alive
|
||||
details.onclick = _ => this.toggleDetails(details);
|
||||
this.td(tr, entry.percentage + "%", 'percentage');
|
||||
this.td(tr, entry.count, 'count');
|
||||
this.td(tr, this.processValue(entry.key), 'key');
|
||||
fragment.appendChild(tr);
|
||||
node.className = className;
|
||||
tr.appendChild(node);
|
||||
return node
|
||||
}
|
||||
let omitted = entries.length - max;
|
||||
if (omitted > 0) {
|
||||
let tr = document.createElement("tr");
|
||||
let tdNode = this.td(tr, 'Omitted ' + omitted + " entries.");
|
||||
tdNode.colSpan = 4;
|
||||
fragment.appendChild(tr);
|
||||
|
||||
handleMapClick(e) {
|
||||
this.dispatchEvent(new SelectEvent(e.target.parentNode.entry));
|
||||
}
|
||||
parent.appendChild(fragment);
|
||||
}
|
||||
|
||||
|
||||
renderDrilldown(entry, previousSibling) {
|
||||
let tr = document.createElement('tr');
|
||||
tr.className = "entry-details";
|
||||
tr.style.display = "none";
|
||||
// indent by one td.
|
||||
tr.appendChild(document.createElement("td"));
|
||||
let td = document.createElement("td");
|
||||
td.colSpan = 3;
|
||||
for (let key in entry.groups) {
|
||||
td.appendChild(this.renderDrilldownGroup(entry, key));
|
||||
handleFilePositionClick(e) {
|
||||
this.dispatchEvent(new SelectEvent(e.target.parentNode.entry.key));
|
||||
}
|
||||
tr.appendChild(td);
|
||||
// Append the new TR after previousSibling.
|
||||
previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling)
|
||||
}
|
||||
|
||||
renderDrilldownGroup(entry, key) {
|
||||
let max = 20;
|
||||
let group = entry.groups[key];
|
||||
let div = document.createElement("div")
|
||||
div.className = 'drilldown-group-title'
|
||||
div.textContent = key + ' [top ' + max + ' out of ' + group.length + ']';
|
||||
let table = document.createElement("table");
|
||||
this.render(group.slice(0, max), table, false)
|
||||
div.appendChild(table);
|
||||
return div;
|
||||
}
|
||||
|
||||
toggleDetails(node) {
|
||||
let tr = node.parentNode;
|
||||
let entry = tr.entry;
|
||||
// Create subgroup in-place if the don't exist yet.
|
||||
if (entry.groups === undefined) {
|
||||
entry.createSubGroups();
|
||||
this.renderDrilldown(entry, tr);
|
||||
render(entries, parent) {
|
||||
let fragment = document.createDocumentFragment();
|
||||
let max = Math.min(1000, entries.length)
|
||||
for (let i = 0; i < max; i++) {
|
||||
let entry = entries[i];
|
||||
let tr = document.createElement("tr");
|
||||
tr.entry = entry;
|
||||
//TODO(zcankara) Create one bound method and use it everywhere
|
||||
if (entry.property === "map") {
|
||||
tr.addEventListener('click', e => this.handleMapClick(e));
|
||||
} else if (entry.property == "filePosition") {
|
||||
tr.addEventListener('click',
|
||||
e => this.handleFilePositionClick(e));
|
||||
}
|
||||
let details = this.td(tr, '<span>ℹ</a>', 'details');
|
||||
//TODO(zcankara) don't keep the whole function context alive
|
||||
details.onclick = _ => this.toggleDetails(details);
|
||||
this.td(tr, entry.percentage + "%", 'percentage');
|
||||
this.td(tr, entry.count, 'count');
|
||||
this.td(tr, this.processValue(entry.key), 'key');
|
||||
fragment.appendChild(tr);
|
||||
}
|
||||
let omitted = entries.length - max;
|
||||
if (omitted > 0) {
|
||||
let tr = document.createElement("tr");
|
||||
let tdNode = this.td(tr, 'Omitted ' + omitted + " entries.");
|
||||
tdNode.colSpan = 4;
|
||||
fragment.appendChild(tr);
|
||||
}
|
||||
parent.appendChild(fragment);
|
||||
}
|
||||
let details = tr.nextSibling;
|
||||
let display = details.style.display;
|
||||
if (display != "none") {
|
||||
display = "none";
|
||||
} else {
|
||||
display = "table-row"
|
||||
};
|
||||
details.style.display = display;
|
||||
}
|
||||
|
||||
initGroupKeySelect() {
|
||||
let select = this.groupKey;
|
||||
select.options.length = 0;
|
||||
for (let i in CustomIcProcessor.kProperties) {
|
||||
let option = document.createElement("option");
|
||||
option.text = CustomIcProcessor.kProperties[i];
|
||||
select.add(option);
|
||||
|
||||
renderDrilldown(entry, previousSibling) {
|
||||
let tr = document.createElement('tr');
|
||||
tr.className = "entry-details";
|
||||
tr.style.display = "none";
|
||||
// indent by one td.
|
||||
tr.appendChild(document.createElement("td"));
|
||||
let td = document.createElement("td");
|
||||
td.colSpan = 3;
|
||||
for (let key in entry.groups) {
|
||||
td.appendChild(this.renderDrilldownGroup(entry, key));
|
||||
}
|
||||
tr.appendChild(td);
|
||||
// Append the new TR after previousSibling.
|
||||
previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling)
|
||||
}
|
||||
}
|
||||
|
||||
handleICTimeFilter(e) {
|
||||
const startTime = parseInt(this.$('#filter-time-start').value);
|
||||
const endTime = parseInt(this.$('#filter-time-end').value);
|
||||
const dataModel = {startTime, endTime};
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'ictimefilter', {bubbles: true, composed: true, detail: dataModel}));
|
||||
}
|
||||
renderDrilldownGroup(entry, key) {
|
||||
let max = 20;
|
||||
let group = entry.groups[key];
|
||||
let div = document.createElement("div")
|
||||
div.className = 'drilldown-group-title'
|
||||
div.textContent = key + ' [top ' + max + ' out of ' + group.length + ']';
|
||||
let table = document.createElement("table");
|
||||
this.render(group.slice(0, max), table, false)
|
||||
div.appendChild(table);
|
||||
return div;
|
||||
}
|
||||
|
||||
});
|
||||
toggleDetails(node) {
|
||||
let tr = node.parentNode;
|
||||
let entry = tr.entry;
|
||||
// Create subgroup in-place if the don't exist yet.
|
||||
if (entry.groups === undefined) {
|
||||
entry.createSubGroups();
|
||||
this.renderDrilldown(entry, tr);
|
||||
}
|
||||
let details = tr.nextSibling;
|
||||
let display = details.style.display;
|
||||
if (display != "none") {
|
||||
display = "none";
|
||||
} else {
|
||||
display = "table-row"
|
||||
};
|
||||
details.style.display = display;
|
||||
}
|
||||
|
||||
initGroupKeySelect() {
|
||||
let select = this.groupKey;
|
||||
select.options.length = 0;
|
||||
for (let i in CustomIcProcessor.kProperties) {
|
||||
let option = document.createElement("option");
|
||||
option.text = CustomIcProcessor.kProperties[i];
|
||||
select.add(option);
|
||||
}
|
||||
}
|
||||
|
||||
handleICTimeFilter(e) {
|
||||
this.dispatchEvent(new SelectTimeEvent(
|
||||
parseInt(this.$('#filter-time-start').value),
|
||||
parseInt(this.$('#filter-time-end').value)));
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ import CustomIcProcessor from "./ic-processor.mjs";
|
||||
import { Entry } from "./ic-processor.mjs";
|
||||
import { State } from "./app-model.mjs";
|
||||
import { MapProcessor, V8Map } from "./map-processor.mjs";
|
||||
import { SelectTimeEvent } from "./events.mjs";
|
||||
import { $ } from "./helper.mjs";
|
||||
import "./ic-panel.mjs";
|
||||
import "./timeline-panel.mjs";
|
||||
@ -38,26 +39,28 @@ class App {
|
||||
this.handleDataUpload(e)
|
||||
);
|
||||
Object.entries(this.#view).forEach(([_, value]) => {
|
||||
value.addEventListener("showentries", (e) => this.handleShowEntries(e));
|
||||
value.addEventListener("showentrydetail", (e) =>
|
||||
this.handleShowEntryDetail(e)
|
||||
);
|
||||
value.addEventListener('showentries',
|
||||
e => this.handleShowEntries(e));
|
||||
value.addEventListener('showentrydetail',
|
||||
e => this.handleShowEntryDetail(e));
|
||||
value.addEventListener(SelectTimeEvent.name,
|
||||
e => this.handleTimeRangeSelect(e));
|
||||
});
|
||||
this.#view.icPanel.addEventListener("ictimefilter", (e) =>
|
||||
this.handleICTimeFilter(e)
|
||||
);
|
||||
}
|
||||
handleShowEntries(e) {
|
||||
if (e.entries[0] instanceof V8Map) {
|
||||
this.#view.mapPanel.mapEntries = e.entries;
|
||||
this.showMapEntries(e.entries);
|
||||
}
|
||||
}
|
||||
handleTimeRangeSelect(e) {
|
||||
this.selectTimeRange(e.start, e.end);
|
||||
}
|
||||
handleShowEntryDetail(e) {
|
||||
if (e.entry instanceof V8Map) {
|
||||
this.selectMapLogEvent(e.entry);
|
||||
} else if (e.entry instanceof Entry) {
|
||||
this.selectICLogEvent(e.entry);
|
||||
} else if (typeof e.entry === "string") {
|
||||
} else if (typeof e.entry === 'string') {
|
||||
this.selectSourcePositionEvent(e.entry);
|
||||
} else {
|
||||
console.log("undefined");
|
||||
@ -67,6 +70,18 @@ class App {
|
||||
//TODO(zcankara) Handle source position
|
||||
console.log("Entry containing source position: ", e.entries);
|
||||
}
|
||||
selectTimeRange(start, end) {
|
||||
this.#state.timeSelection.start = start;
|
||||
this.#state.timeSelection.end = end;
|
||||
this.#state.icTimeline.selectTimeRange(start, end);
|
||||
this.#state.mapTimeline.selectTimeRange(start, end);
|
||||
this.#view.mapPanel.selectedMapLogEvents = this.#state.mapTimeline.selection;
|
||||
this.#view.icPanel.filteredEntries = this.#state.icTimeline.selection;
|
||||
}
|
||||
showMapEntries(entries) {
|
||||
this.#state.selectedMapLogEvents = entries;
|
||||
this.#view.mapPanel.selectedMapLogEvents = this.#state.selectedMapLogEvents;
|
||||
}
|
||||
selectMapLogEvent(entry) {
|
||||
this.#state.map = entry;
|
||||
this.#view.mapTrack.selectedEntry = entry;
|
||||
@ -78,15 +93,6 @@ class App {
|
||||
selectSourcePositionEvent(sourcePositions) {
|
||||
console.log("source positions: ", sourcePositions);
|
||||
}
|
||||
handleICTimeFilter(event) {
|
||||
this.#state.timeSelection.start = event.detail.startTime;
|
||||
this.#state.timeSelection.end = event.detail.endTime;
|
||||
this.#view.icTrack.data.selectTimeRange(
|
||||
this.#state.timeSelection.start,
|
||||
this.#state.timeSelection.end
|
||||
);
|
||||
this.#view.icPanel.filteredEntries = this.#view.icTrack.data.selection;
|
||||
}
|
||||
handleFileUpload(e) {
|
||||
$("#container").className = "initial";
|
||||
}
|
||||
|
@ -4,88 +4,88 @@
|
||||
import "./stats-panel.mjs";
|
||||
import "./map-panel/map-details.mjs";
|
||||
import "./map-panel/map-transitions.mjs";
|
||||
import {SelectEvent} from './events.mjs';
|
||||
import {V8Map} from "./map-processor.mjs";
|
||||
import {defineCustomElement, V8CustomElement} from './helper.mjs';
|
||||
import { SelectEvent } from './events.mjs';
|
||||
import { V8Map } from "./map-processor.mjs";
|
||||
import { defineCustomElement, V8CustomElement } from './helper.mjs';
|
||||
|
||||
defineCustomElement('map-panel', (templateText) =>
|
||||
class MapPanel extends V8CustomElement {
|
||||
#map;
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.searchBarBtn.addEventListener(
|
||||
class MapPanel extends V8CustomElement {
|
||||
#map;
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.searchBarBtn.addEventListener(
|
||||
'click', e => this.handleSearchBar(e));
|
||||
this.addEventListener(
|
||||
'mapdetailsupdate', e => this.handleUpdateMapDetails(e));
|
||||
}
|
||||
|
||||
handleUpdateMapDetails(e){
|
||||
this.mapDetailsPanel.mapDetails = e.detail;
|
||||
}
|
||||
|
||||
get statsPanel() {
|
||||
return this.$('#stats-panel');
|
||||
}
|
||||
|
||||
get mapTransitionsPanel() {
|
||||
return this.$('#map-transitions');
|
||||
}
|
||||
|
||||
get mapDetailsPanel() {
|
||||
return this.$('#map-details');
|
||||
}
|
||||
|
||||
get searchBarBtn() {
|
||||
return this.$('#searchBarBtn');
|
||||
}
|
||||
|
||||
get searchBar() {
|
||||
return this.$('#searchBar');
|
||||
}
|
||||
|
||||
get mapDetails() {
|
||||
return this.mapDetailsPanel.mapDetails;
|
||||
}
|
||||
|
||||
// send a timeline to the stats-panel
|
||||
get timeline() {
|
||||
return this.statsPanel.timeline;
|
||||
}
|
||||
set timeline(value) {
|
||||
console.assert(value !== undefined, "timeline undefined!");
|
||||
this.statsPanel.timeline = value;
|
||||
this.statsPanel.update();
|
||||
}
|
||||
get transitions() {
|
||||
return this.statsPanel.transitions;
|
||||
}
|
||||
set transitions(value) {
|
||||
this.statsPanel.transitions = value;
|
||||
}
|
||||
|
||||
set map(value) {
|
||||
this.#map = value;
|
||||
this.mapTransitionsPanel.map = this.#map;
|
||||
}
|
||||
|
||||
handleSearchBar(e){
|
||||
let searchBar = this.$('#searchBarInput');
|
||||
let searchBarInput = searchBar.value;
|
||||
//access the map from model cache
|
||||
let selectedMap = V8Map.get(searchBarInput);
|
||||
if(selectedMap){
|
||||
searchBar.className = "success";
|
||||
} else {
|
||||
searchBar.className = "failure";
|
||||
this.addEventListener(
|
||||
'mapdetailsupdate', e => this.handleUpdateMapDetails(e));
|
||||
}
|
||||
this.dispatchEvent(new SelectEvent(selectedMap));
|
||||
}
|
||||
|
||||
set mapEntries(list){
|
||||
this.mapTransitionsPanel.mapEntries = list;
|
||||
}
|
||||
get mapEntries(){
|
||||
return this.mapTransitionsPanel.mapEntries;
|
||||
}
|
||||
handleUpdateMapDetails(e) {
|
||||
this.mapDetailsPanel.mapDetails = e.detail;
|
||||
}
|
||||
|
||||
});
|
||||
get statsPanel() {
|
||||
return this.$('#stats-panel');
|
||||
}
|
||||
|
||||
get mapTransitionsPanel() {
|
||||
return this.$('#map-transitions');
|
||||
}
|
||||
|
||||
get mapDetailsPanel() {
|
||||
return this.$('#map-details');
|
||||
}
|
||||
|
||||
get searchBarBtn() {
|
||||
return this.$('#searchBarBtn');
|
||||
}
|
||||
|
||||
get searchBar() {
|
||||
return this.$('#searchBar');
|
||||
}
|
||||
|
||||
get mapDetails() {
|
||||
return this.mapDetailsPanel.mapDetails;
|
||||
}
|
||||
|
||||
// send a timeline to the stats-panel
|
||||
get timeline() {
|
||||
return this.statsPanel.timeline;
|
||||
}
|
||||
set timeline(value) {
|
||||
console.assert(value !== undefined, "timeline undefined!");
|
||||
this.statsPanel.timeline = value;
|
||||
this.statsPanel.update();
|
||||
}
|
||||
get transitions() {
|
||||
return this.statsPanel.transitions;
|
||||
}
|
||||
set transitions(value) {
|
||||
this.statsPanel.transitions = value;
|
||||
}
|
||||
|
||||
set map(value) {
|
||||
this.#map = value;
|
||||
this.mapTransitionsPanel.map = this.#map;
|
||||
}
|
||||
|
||||
handleSearchBar(e) {
|
||||
let searchBar = this.$('#searchBarInput');
|
||||
let searchBarInput = searchBar.value;
|
||||
//access the map from model cache
|
||||
let selectedMap = V8Map.get(searchBarInput);
|
||||
if (selectedMap) {
|
||||
searchBar.className = "success";
|
||||
} else {
|
||||
searchBar.className = "failure";
|
||||
}
|
||||
this.dispatchEvent(new SelectEvent(selectedMap));
|
||||
}
|
||||
|
||||
set selectedMapLogEvents(list) {
|
||||
this.mapTransitionsPanel.selectedMapLogEvents = list;
|
||||
}
|
||||
get selectedMapLogEvents() {
|
||||
return this.mapTransitionsPanel.selectedMapLogEvents;
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -1,185 +1,185 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import {V8CustomElement, defineCustomElement} from '../helper.mjs';
|
||||
import {SelectEvent} from '../events.mjs';
|
||||
import { V8CustomElement, defineCustomElement } from '../helper.mjs';
|
||||
import { SelectEvent } from '../events.mjs';
|
||||
|
||||
defineCustomElement('./map-panel/map-transitions', (templateText) =>
|
||||
class MapTransitions extends V8CustomElement {
|
||||
#map;
|
||||
#mapEntries;
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.transitionView.addEventListener(
|
||||
'mousemove', e => this.handleTransitionViewChange(e));
|
||||
this.currentNode = this.transitionView;
|
||||
this.currentMap = undefined;
|
||||
}
|
||||
class MapTransitions extends V8CustomElement {
|
||||
#map;
|
||||
#selectedMapLogEvents;
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.transitionView.addEventListener(
|
||||
'mousemove', e => this.handleTransitionViewChange(e));
|
||||
this.currentNode = this.transitionView;
|
||||
this.currentMap = undefined;
|
||||
}
|
||||
|
||||
get transitionView() {
|
||||
return this.$('#transitionView');
|
||||
}
|
||||
get transitionView() {
|
||||
return this.$('#transitionView');
|
||||
}
|
||||
|
||||
get tooltip() {
|
||||
return this.$('#tooltip');
|
||||
}
|
||||
get tooltip() {
|
||||
return this.$('#tooltip');
|
||||
}
|
||||
|
||||
get tooltipContents() {
|
||||
return this.$('#tooltipContents');
|
||||
}
|
||||
get tooltipContents() {
|
||||
return this.$('#tooltipContents');
|
||||
}
|
||||
|
||||
set map(value) {
|
||||
this.#map = value;
|
||||
this.showMap();
|
||||
}
|
||||
set map(value) {
|
||||
this.#map = value;
|
||||
this.showMap();
|
||||
}
|
||||
|
||||
handleTransitionViewChange(e){
|
||||
this.tooltip.style.left = e.pageX + "px";
|
||||
this.tooltip.style.top = e.pageY + "px";
|
||||
let map = e.target.map;
|
||||
if (map) {
|
||||
handleTransitionViewChange(e) {
|
||||
this.tooltip.style.left = e.pageX + "px";
|
||||
this.tooltip.style.top = e.pageY + "px";
|
||||
let map = e.target.map;
|
||||
if (map) {
|
||||
this.tooltipContents.innerText = map.description;
|
||||
}
|
||||
}
|
||||
|
||||
selectMap(map) {
|
||||
this.currentMap = map;
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'mapdetailsupdate', {bubbles: true, composed: true, detail: map}));
|
||||
this.dispatchEvent(new SelectEvent(map));
|
||||
}
|
||||
|
||||
dblClickSelectMap(map) {
|
||||
this.dispatchEvent(new SelectEvent(map));
|
||||
}
|
||||
|
||||
showMap() {
|
||||
// Called when a map selected
|
||||
let selected = this.#map;
|
||||
if (this.currentMap === this.#map) return;
|
||||
this.currentMap = this.#map;
|
||||
this.mapEntries = [this.#map];
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'mapdetailsupdate', {bubbles: true, composed: true, detail: selected}));
|
||||
}
|
||||
|
||||
showMaps() {
|
||||
// Timeline dbl click to show map transitions of selected maps
|
||||
this.transitionView.style.display = 'none';
|
||||
this.removeAllChildren(this.transitionView);
|
||||
this.mapEntries.forEach(map => this.addMapAndParentTransitions(map));
|
||||
this.transitionView.style.display = '';
|
||||
}
|
||||
|
||||
set mapEntries(list){
|
||||
this.#mapEntries = list;
|
||||
this.showMaps();
|
||||
}
|
||||
|
||||
get mapEntries(){
|
||||
return this.#mapEntries;
|
||||
}
|
||||
|
||||
addMapAndParentTransitions(map) {
|
||||
if (map === void 0) return;
|
||||
this.currentNode = this.transitionView;
|
||||
let parents = map.getParents();
|
||||
if (parents.length > 0) {
|
||||
this.addTransitionTo(parents.pop());
|
||||
parents.reverse().forEach(each => this.addTransitionTo(each));
|
||||
}
|
||||
let mapNode = this.addSubtransitions(map);
|
||||
// Mark and show the selected map.
|
||||
mapNode.classList.add('selected');
|
||||
if (this.selectedMap == map) {
|
||||
setTimeout(
|
||||
() => mapNode.scrollIntoView(
|
||||
{behavior: 'smooth', block: 'nearest', inline: 'nearest'}),
|
||||
1);
|
||||
}
|
||||
}
|
||||
|
||||
addMapNode(map) {
|
||||
let node = this.div('map');
|
||||
if (map.edge) node.style.backgroundColor = map.edge.getColor();
|
||||
node.map = map;
|
||||
node.addEventListener('click', () => this.selectMap(map));
|
||||
node.addEventListener('dblclick', () => this.dblClickSelectMap(map));
|
||||
if (map.children.length > 1) {
|
||||
node.innerText = map.children.length;
|
||||
let showSubtree = this.div('showSubtransitions');
|
||||
showSubtree.addEventListener('click', (e) => this.toggleSubtree(e, node));
|
||||
node.appendChild(showSubtree);
|
||||
} else if (map.children.length == 0) {
|
||||
node.innerHTML = '●'
|
||||
}
|
||||
this.currentNode.appendChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
addSubtransitions(map) {
|
||||
let mapNode = this.addTransitionTo(map);
|
||||
// Draw outgoing linear transition line.
|
||||
let current = map;
|
||||
while (current.children.length == 1) {
|
||||
current = current.children[0].to;
|
||||
this.addTransitionTo(current);
|
||||
}
|
||||
return mapNode;
|
||||
}
|
||||
|
||||
addTransitionEdge(map) {
|
||||
let classes = ['transitionEdge'];
|
||||
let edge = this.div(classes);
|
||||
edge.style.backgroundColor = map.edge.getColor();
|
||||
let labelNode = this.div('transitionLabel');
|
||||
labelNode.innerText = map.edge.toString();
|
||||
edge.appendChild(labelNode);
|
||||
return edge;
|
||||
}
|
||||
|
||||
addTransitionTo(map) {
|
||||
// transition[ transitions[ transition[...], transition[...], ...]];
|
||||
|
||||
let transition = this.div('transition');
|
||||
if (map.isDeprecated()) transition.classList.add('deprecated');
|
||||
if (map.edge) {
|
||||
transition.appendChild(this.addTransitionEdge(map));
|
||||
}
|
||||
let mapNode = this.addMapNode(map);
|
||||
transition.appendChild(mapNode);
|
||||
|
||||
let subtree = this.div('transitions');
|
||||
transition.appendChild(subtree);
|
||||
|
||||
this.currentNode.appendChild(transition);
|
||||
this.currentNode = subtree;
|
||||
|
||||
return mapNode;
|
||||
}
|
||||
|
||||
toggleSubtree(event, node) {
|
||||
let map = node.map;
|
||||
event.target.classList.toggle('opened');
|
||||
let transitionsNode = node.parentElement.querySelector('.transitions');
|
||||
let subtransitionNodes = transitionsNode.children;
|
||||
if (subtransitionNodes.length <= 1) {
|
||||
// Add subtransitions excepth the one that's already shown.
|
||||
let visibleTransitionMap = subtransitionNodes.length == 1 ?
|
||||
transitionsNode.querySelector('.map').map :
|
||||
void 0;
|
||||
map.children.forEach(edge => {
|
||||
if (edge.to != visibleTransitionMap) {
|
||||
this.currentNode = transitionsNode;
|
||||
this.addSubtransitions(edge.to);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// remove all but the first (currently selected) subtransition
|
||||
for (let i = subtransitionNodes.length - 1; i > 0; i--) {
|
||||
transitionsNode.removeChild(subtransitionNodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
selectMap(map) {
|
||||
this.currentMap = map;
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'mapdetailsupdate', { bubbles: true, composed: true, detail: map }));
|
||||
this.dispatchEvent(new SelectEvent(map));
|
||||
}
|
||||
|
||||
dblClickSelectMap(map) {
|
||||
this.dispatchEvent(new SelectEvent(map));
|
||||
}
|
||||
|
||||
showMap() {
|
||||
// Called when a map selected
|
||||
let selected = this.#map;
|
||||
if (this.currentMap === this.#map) return;
|
||||
this.currentMap = this.#map;
|
||||
this.selectedMapLogEvents = [this.#map];
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'mapdetailsupdate', { bubbles: true, composed: true, detail: selected }));
|
||||
}
|
||||
|
||||
showMaps() {
|
||||
// Timeline dbl click to show map transitions of selected maps
|
||||
this.transitionView.style.display = 'none';
|
||||
this.removeAllChildren(this.transitionView);
|
||||
this.selectedMapLogEvents.forEach(map => this.addMapAndParentTransitions(map));
|
||||
this.transitionView.style.display = '';
|
||||
}
|
||||
|
||||
set selectedMapLogEvents(list) {
|
||||
this.#selectedMapLogEvents = list;
|
||||
this.showMaps();
|
||||
}
|
||||
|
||||
get selectedMapLogEvents() {
|
||||
return this.#selectedMapLogEvents;
|
||||
}
|
||||
|
||||
addMapAndParentTransitions(map) {
|
||||
if (map === void 0) return;
|
||||
this.currentNode = this.transitionView;
|
||||
let parents = map.getParents();
|
||||
if (parents.length > 0) {
|
||||
this.addTransitionTo(parents.pop());
|
||||
parents.reverse().forEach(each => this.addTransitionTo(each));
|
||||
}
|
||||
let mapNode = this.addSubtransitions(map);
|
||||
// Mark and show the selected map.
|
||||
mapNode.classList.add('selected');
|
||||
if (this.selectedMap == map) {
|
||||
setTimeout(
|
||||
() => mapNode.scrollIntoView(
|
||||
{ behavior: 'smooth', block: 'nearest', inline: 'nearest' }),
|
||||
1);
|
||||
}
|
||||
}
|
||||
|
||||
addMapNode(map) {
|
||||
let node = this.div('map');
|
||||
if (map.edge) node.style.backgroundColor = map.edge.getColor();
|
||||
node.map = map;
|
||||
node.addEventListener('click', () => this.selectMap(map));
|
||||
node.addEventListener('dblclick', () => this.dblClickSelectMap(map));
|
||||
if (map.children.length > 1) {
|
||||
node.innerText = map.children.length;
|
||||
let showSubtree = this.div('showSubtransitions');
|
||||
showSubtree.addEventListener('click', (e) => this.toggleSubtree(e, node));
|
||||
node.appendChild(showSubtree);
|
||||
} else if (map.children.length == 0) {
|
||||
node.innerHTML = '●'
|
||||
}
|
||||
this.currentNode.appendChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
addSubtransitions(map) {
|
||||
let mapNode = this.addTransitionTo(map);
|
||||
// Draw outgoing linear transition line.
|
||||
let current = map;
|
||||
while (current.children.length == 1) {
|
||||
current = current.children[0].to;
|
||||
this.addTransitionTo(current);
|
||||
}
|
||||
return mapNode;
|
||||
}
|
||||
|
||||
addTransitionEdge(map) {
|
||||
let classes = ['transitionEdge'];
|
||||
let edge = this.div(classes);
|
||||
edge.style.backgroundColor = map.edge.getColor();
|
||||
let labelNode = this.div('transitionLabel');
|
||||
labelNode.innerText = map.edge.toString();
|
||||
edge.appendChild(labelNode);
|
||||
return edge;
|
||||
}
|
||||
|
||||
addTransitionTo(map) {
|
||||
// transition[ transitions[ transition[...], transition[...], ...]];
|
||||
|
||||
let transition = this.div('transition');
|
||||
if (map.isDeprecated()) transition.classList.add('deprecated');
|
||||
if (map.edge) {
|
||||
transition.appendChild(this.addTransitionEdge(map));
|
||||
}
|
||||
let mapNode = this.addMapNode(map);
|
||||
transition.appendChild(mapNode);
|
||||
|
||||
let subtree = this.div('transitions');
|
||||
transition.appendChild(subtree);
|
||||
|
||||
this.currentNode.appendChild(transition);
|
||||
this.currentNode = subtree;
|
||||
|
||||
return mapNode;
|
||||
}
|
||||
|
||||
toggleSubtree(event, node) {
|
||||
let map = node.map;
|
||||
event.target.classList.toggle('opened');
|
||||
let transitionsNode = node.parentElement.querySelector('.transitions');
|
||||
let subtransitionNodes = transitionsNode.children;
|
||||
if (subtransitionNodes.length <= 1) {
|
||||
// Add subtransitions excepth the one that's already shown.
|
||||
let visibleTransitionMap = subtransitionNodes.length == 1 ?
|
||||
transitionsNode.querySelector('.map').map :
|
||||
void 0;
|
||||
map.children.forEach(edge => {
|
||||
if (edge.to != visibleTransitionMap) {
|
||||
this.currentNode = transitionsNode;
|
||||
this.addSubtransitions(edge.to);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// remove all but the first (currently selected) subtransition
|
||||
for (let i = subtransitionNodes.length - 1; i > 0; i--) {
|
||||
transitionsNode.removeChild(subtransitionNodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -2,10 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import {defineCustomElement, V8CustomElement,
|
||||
transitionTypeToColor, CSSColor} from '../helper.mjs';
|
||||
import {kChunkWidth, kChunkHeight} from '../map-processor.mjs';
|
||||
import {SelectionEvent, SelectEvent} from '../events.mjs';
|
||||
import {
|
||||
defineCustomElement, V8CustomElement,
|
||||
transitionTypeToColor, CSSColor
|
||||
} from '../helper.mjs';
|
||||
import { kChunkWidth, kChunkHeight } from '../map-processor.mjs';
|
||||
import { SelectionEvent, SelectEvent, SelectTimeEvent } from '../events.mjs';
|
||||
|
||||
defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
class TimelineTrack extends V8CustomElement {
|
||||
@ -13,8 +15,14 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
#nofChunks = 400;
|
||||
#chunks;
|
||||
#selectedEntry;
|
||||
#timeToPixel;
|
||||
#timeSelection = { start: 0, end: Infinity };
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.timeline.addEventListener("mousedown",
|
||||
e => this.handleTimeRangeSelectionStart(e));
|
||||
this.timeline.addEventListener("mouseup",
|
||||
e => this.handleTimeRangeSelectionEnd(e));
|
||||
this.timeline.addEventListener("scroll",
|
||||
e => this.handleTimelineScroll(e));
|
||||
this.backgroundCanvas = document.createElement('canvas');
|
||||
@ -48,36 +56,36 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
return this.#timeline;
|
||||
}
|
||||
|
||||
set nofChunks(count){
|
||||
set nofChunks(count) {
|
||||
this.#nofChunks = count;
|
||||
this.updateChunks();
|
||||
this.updateTimeline();
|
||||
}
|
||||
get nofChunks(){
|
||||
get nofChunks() {
|
||||
return this.#nofChunks;
|
||||
}
|
||||
updateChunks() {
|
||||
this.#chunks = this.data.chunks(this.nofChunks);
|
||||
}
|
||||
get chunks(){
|
||||
get chunks() {
|
||||
return this.#chunks;
|
||||
}
|
||||
set selectedEntry(value){
|
||||
set selectedEntry(value) {
|
||||
this.#selectedEntry = value;
|
||||
if(value.edge) this.redraw();
|
||||
if (value.edge) this.redraw();
|
||||
}
|
||||
get selectedEntry(){
|
||||
get selectedEntry() {
|
||||
return this.#selectedEntry;
|
||||
}
|
||||
|
||||
set scrollLeft(offset){
|
||||
set scrollLeft(offset) {
|
||||
this.timeline.scrollLeft = offset;
|
||||
}
|
||||
|
||||
updateStats(){
|
||||
updateStats() {
|
||||
let unique = new Map();
|
||||
for (const entry of this.data.all) {
|
||||
if(!unique.has(entry.type)) {
|
||||
if (!unique.has(entry.type)) {
|
||||
unique.set(entry.type, [entry]);
|
||||
} else {
|
||||
unique.get(entry.type).push(entry);
|
||||
@ -86,7 +94,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
this.renderStatsWindow(unique);
|
||||
}
|
||||
|
||||
renderStatsWindow(unique){
|
||||
renderStatsWindow(unique) {
|
||||
let timelineLegendContent = this.timelineLegendContent;
|
||||
this.removeAllChildren(timelineLegendContent);
|
||||
let fragment = document.createDocumentFragment();
|
||||
@ -107,7 +115,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
timelineLegendContent.appendChild(fragment);
|
||||
}
|
||||
|
||||
handleEntryTypeDblClick(e){
|
||||
handleEntryTypeDblClick(e) {
|
||||
this.dispatchEvent(new SelectionEvent(e.target.entries));
|
||||
}
|
||||
|
||||
@ -115,11 +123,31 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
this.timeline.scrollLeft += offset;
|
||||
}
|
||||
|
||||
handleTimelineScroll(e){
|
||||
handleTimeRangeSelectionStart(e) {
|
||||
this.#timeSelection.start = this.positionToTime(e.clientX);
|
||||
}
|
||||
|
||||
handleTimeRangeSelectionEnd(e) {
|
||||
this.#timeSelection.end = this.positionToTime(e.clientX);
|
||||
this.dispatchEvent(new SelectTimeEvent(
|
||||
Math.min(this.#timeSelection.start, this.#timeSelection.end),
|
||||
Math.max(this.#timeSelection.start, this.#timeSelection.end)));
|
||||
}
|
||||
|
||||
positionToTime(posX) {
|
||||
let rect = this.timeline.getBoundingClientRect();
|
||||
let posClickedX = posX - rect.left + this.timeline.scrollLeft;
|
||||
let selectedTime = posClickedX / this.#timeToPixel;
|
||||
return selectedTime;
|
||||
}
|
||||
|
||||
handleTimelineScroll(e) {
|
||||
let horizontal = e.currentTarget.scrollLeft;
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'scrolltrack', {bubbles: true, composed: true,
|
||||
detail: horizontal}));
|
||||
'scrolltrack', {
|
||||
bubbles: true, composed: true,
|
||||
detail: horizontal
|
||||
}));
|
||||
}
|
||||
|
||||
asyncSetTimelineChunkBackground(backgroundTodo) {
|
||||
@ -176,11 +204,11 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
let start = this.data.startTime;
|
||||
let end = this.data.endTime;
|
||||
let duration = end - start;
|
||||
const timeToPixel = chunks.length * kChunkWidth / duration;
|
||||
this.#timeToPixel = chunks.length * kChunkWidth / duration;
|
||||
let addTimestamp = (time, name) => {
|
||||
let timeNode = this.div('timestamp');
|
||||
timeNode.innerText = name;
|
||||
timeNode.style.left = ((time - start) * timeToPixel) + 'px';
|
||||
timeNode.style.left = ((time - start) * this.#timeToPixel) + 'px';
|
||||
chunksNode.appendChild(timeNode);
|
||||
};
|
||||
let backgroundTodo = [];
|
||||
@ -224,7 +252,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
if (!chunk) return;
|
||||
// topmost map (at chunk.height) == map #0.
|
||||
let relativeIndex =
|
||||
Math.round(event.layerY / event.target.offsetHeight * chunk.size());
|
||||
Math.round(event.layerY / event.target.offsetHeight * chunk.size());
|
||||
let map = chunk.at(relativeIndex);
|
||||
this.dispatchEvent(new SelectEvent(map));
|
||||
}
|
||||
@ -264,7 +292,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
ctx.fill();
|
||||
let imageData = canvas.toDataURL('image/webp', 0.2);
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'overviewupdate', {bubbles: true, composed: true, detail: imageData}));
|
||||
'overviewupdate', { bubbles: true, composed: true, detail: imageData }));
|
||||
}
|
||||
|
||||
redraw() {
|
||||
@ -384,7 +412,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = CSSColor.onBackgroundColor;
|
||||
ctx.fillText(
|
||||
edge.toString(), centerX + offsetX + 2, centerY - labelOffset);
|
||||
edge.toString(), centerX + offsetX + 2, centerY - labelOffset);
|
||||
}
|
||||
return [xTo, yTo];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user