2017-12-12 13:35:33 +00:00
|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<!--
|
|
|
|
Copyright 2017 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.
|
|
|
|
-->
|
|
|
|
<head>
|
2019-01-04 12:18:43 +00:00
|
|
|
<meta charset="utf-8">
|
2017-12-12 13:35:33 +00:00
|
|
|
<style>
|
|
|
|
html, body {
|
|
|
|
font-family: sans-serif;
|
|
|
|
padding: 0px;
|
|
|
|
margin: 0px;
|
|
|
|
}
|
|
|
|
h1, h2, h3, section {
|
|
|
|
padding-left: 15px;
|
|
|
|
}
|
2018-11-20 21:59:46 +00:00
|
|
|
|
2020-04-08 20:41:02 +00:00
|
|
|
kbd {
|
|
|
|
background-color: #eee;
|
|
|
|
border-radius: 3px;
|
|
|
|
border: 1px solid black;
|
|
|
|
display: inline-block;
|
|
|
|
font-size: .9em;
|
|
|
|
font-weight: bold;
|
|
|
|
padding: 0px 4px 2px 4px;
|
|
|
|
white-space: nowrap;
|
|
|
|
}
|
|
|
|
dl {
|
|
|
|
display: grid;
|
|
|
|
grid-template-columns: min-content auto;
|
|
|
|
grid-gap: 10px;
|
|
|
|
}
|
|
|
|
dt {
|
|
|
|
text-align: right;
|
|
|
|
white-space: nowrap;
|
|
|
|
}
|
|
|
|
dd {
|
|
|
|
margin: 0;
|
|
|
|
}
|
|
|
|
|
2018-11-20 21:59:46 +00:00
|
|
|
#content {
|
|
|
|
opacity: 0.0;
|
|
|
|
height: 0px;
|
|
|
|
transition: all 0.5s ease-in-out;
|
|
|
|
}
|
|
|
|
|
|
|
|
.success #content {
|
|
|
|
height: auto;
|
|
|
|
opacity: 1.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
#fileReader {
|
|
|
|
width: 100%;
|
|
|
|
height: 100px;
|
|
|
|
line-height: 100px;
|
|
|
|
text-align: center;
|
|
|
|
border: solid 1px #000000;
|
|
|
|
border-radius: 5px;
|
|
|
|
cursor: pointer;
|
|
|
|
transition: all 0.5s ease-in-out;
|
|
|
|
}
|
|
|
|
|
|
|
|
.failure #fileReader {
|
|
|
|
background-color: #FFAAAA;
|
|
|
|
}
|
|
|
|
|
|
|
|
.success #fileReader {
|
|
|
|
height: 20px;
|
|
|
|
line-height: 20px;
|
|
|
|
}
|
|
|
|
|
|
|
|
#fileReader:hover {
|
|
|
|
background-color: #e0edfe;
|
|
|
|
}
|
|
|
|
|
|
|
|
.loading #fileReader {
|
|
|
|
cursor: wait;
|
|
|
|
}
|
|
|
|
|
|
|
|
#fileReader > input {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#loader {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.loading #loader {
|
|
|
|
display: block;
|
|
|
|
position: fixed;
|
|
|
|
top: 0px;
|
|
|
|
left: 0px;
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
background-color: rgba(255, 255, 255, 0.5);
|
|
|
|
}
|
|
|
|
|
|
|
|
#spinner {
|
|
|
|
position: absolute;
|
|
|
|
width: 100px;
|
|
|
|
height: 100px;
|
|
|
|
top: 40%;
|
|
|
|
left: 50%;
|
|
|
|
margin-left: -50px;
|
|
|
|
border: 30px solid #000;
|
|
|
|
border-top: 30px solid #36E;
|
|
|
|
border-radius: 50%;
|
|
|
|
animation: spin 1s ease-in-out infinite;
|
|
|
|
}
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
0% {
|
|
|
|
transform: rotate(0deg);
|
|
|
|
}
|
|
|
|
100% {
|
|
|
|
transform: rotate(360deg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.colorbox {
|
|
|
|
width: 10px;
|
|
|
|
height: 10px;
|
|
|
|
border: 1px black solid;
|
|
|
|
}
|
|
|
|
|
2020-04-08 20:41:02 +00:00
|
|
|
#stats {
|
|
|
|
display: flex;
|
|
|
|
height: 250px;
|
|
|
|
}
|
|
|
|
|
2017-12-12 13:35:33 +00:00
|
|
|
#stats table {
|
2020-04-08 20:41:02 +00:00
|
|
|
flex: 1;
|
2017-12-12 13:35:33 +00:00
|
|
|
padding-right: 50px;
|
2020-04-08 20:41:02 +00:00
|
|
|
max-height: 250px;
|
|
|
|
display: inline-block;
|
2017-12-12 13:35:33 +00:00
|
|
|
}
|
2018-11-20 21:59:46 +00:00
|
|
|
#stats table td {
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
2017-12-12 13:35:33 +00:00
|
|
|
#stats .transitionTable {
|
|
|
|
overflow-y: scroll;
|
|
|
|
}
|
2020-04-08 20:41:02 +00:00
|
|
|
#stats .transitionTable tr {
|
|
|
|
max-width: 200px;
|
2020-02-26 16:43:14 +00:00
|
|
|
|
2020-04-08 20:41:02 +00:00
|
|
|
}
|
2020-02-26 16:43:14 +00:00
|
|
|
#stats .transitionType {
|
|
|
|
text-align: right;
|
2020-04-08 20:41:02 +00:00
|
|
|
max-width: 380px;
|
2020-02-26 16:43:14 +00:00
|
|
|
}
|
|
|
|
#stats .transitionType tr td:nth-child(2) {
|
|
|
|
text-align: left;
|
|
|
|
}
|
2020-04-08 20:41:02 +00:00
|
|
|
#stats table thead td {
|
2020-02-26 16:43:14 +00:00
|
|
|
border-bottom: 1px black dotted;
|
|
|
|
}
|
|
|
|
|
2017-12-12 13:35:33 +00:00
|
|
|
#timeline {
|
|
|
|
position: relative;
|
|
|
|
height: 300px;
|
|
|
|
overflow-y: hidden;
|
|
|
|
overflow-x: scroll;
|
|
|
|
user-select: none;
|
|
|
|
}
|
2018-11-20 21:59:46 +00:00
|
|
|
#timelineLabel {
|
|
|
|
transform: rotate(90deg);
|
|
|
|
transform-origin: left bottom 0;
|
|
|
|
position: absolute;
|
|
|
|
left: 0;
|
|
|
|
width: 250px;
|
|
|
|
text-align: center;
|
|
|
|
font-size: 10px;
|
|
|
|
opacity: 0.5;
|
|
|
|
}
|
2017-12-12 13:35:33 +00:00
|
|
|
#timelineChunks {
|
|
|
|
height: 250px;
|
|
|
|
position: absolute;
|
|
|
|
margin-right: 100px;
|
|
|
|
}
|
|
|
|
#timelineCanvas {
|
|
|
|
height: 250px;
|
|
|
|
position: relative;
|
|
|
|
overflow: visible;
|
|
|
|
pointer-events: none;
|
|
|
|
}
|
|
|
|
.chunk {
|
|
|
|
width: 6px;
|
|
|
|
border: 0px white solid;
|
|
|
|
border-width: 0 2px 0 2px;
|
|
|
|
position: absolute;
|
|
|
|
background-size: 100% 100%;
|
|
|
|
image-rendering: pixelated;
|
|
|
|
bottom: 0px;
|
|
|
|
}
|
|
|
|
.timestamp {
|
|
|
|
height: 250px;
|
|
|
|
width: 100px;
|
|
|
|
border-left: 1px black dashed;
|
|
|
|
padding-left: 4px;
|
|
|
|
position: absolute;
|
|
|
|
pointer-events: none;
|
|
|
|
font-size: 10px;
|
|
|
|
opacity: 0.5;
|
|
|
|
}
|
|
|
|
#timelineOverview {
|
|
|
|
width: 100%;
|
|
|
|
height: 50px;
|
|
|
|
position: relative;
|
|
|
|
margin-top: -50px;
|
|
|
|
margin-bottom: 10px;
|
|
|
|
background-size: 100% 100%;
|
|
|
|
border: 1px black solid;
|
|
|
|
border-width: 1px 0 1px 0;
|
|
|
|
overflow: hidden;
|
|
|
|
}
|
|
|
|
#timelineOverviewIndicator {
|
|
|
|
height: 100%;
|
|
|
|
position: absolute;
|
|
|
|
box-shadow: 0px 2px 20px -5px black inset;
|
|
|
|
top: 0px;
|
|
|
|
cursor: ew-resize;
|
|
|
|
}
|
|
|
|
#timelineOverviewIndicator .leftMask,
|
|
|
|
#timelineOverviewIndicator .rightMask {
|
|
|
|
background-color: rgba(200, 200, 200, 0.5);
|
|
|
|
width: 10000px;
|
|
|
|
height: 100%;
|
|
|
|
position: absolute;
|
|
|
|
top: 0px;
|
|
|
|
}
|
|
|
|
#timelineOverviewIndicator .leftMask {
|
|
|
|
right: 100%;
|
|
|
|
}
|
|
|
|
#timelineOverviewIndicator .rightMask {
|
|
|
|
left: 100%;
|
|
|
|
}
|
|
|
|
#mapDetails {
|
|
|
|
font-family: monospace;
|
|
|
|
white-space: pre;
|
|
|
|
}
|
|
|
|
#transitionView {
|
|
|
|
overflow-x: scroll;
|
|
|
|
white-space: nowrap;
|
|
|
|
min-height: 50px;
|
|
|
|
max-height: 200px;
|
|
|
|
padding: 50px 0 0 0;
|
|
|
|
margin-top: -25px;
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
.map {
|
|
|
|
width: 20px;
|
|
|
|
height: 20px;
|
|
|
|
display: inline-block;
|
|
|
|
border-radius: 50%;
|
|
|
|
background-color: black;
|
|
|
|
border: 4px solid white;
|
|
|
|
font-size: 10px;
|
|
|
|
text-align: center;
|
|
|
|
line-height: 18px;
|
|
|
|
color: white;
|
|
|
|
vertical-align: top;
|
|
|
|
margin-top: -13px;
|
|
|
|
/* raise z-index */
|
|
|
|
position: relative;
|
|
|
|
z-index: 2;
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
.map.selected {
|
|
|
|
border-color: black;
|
|
|
|
}
|
|
|
|
.transitions {
|
|
|
|
display: inline-block;
|
|
|
|
margin-left: -15px;
|
|
|
|
}
|
|
|
|
.transition {
|
|
|
|
min-height: 55px;
|
|
|
|
margin: 0 0 -2px 2px;
|
|
|
|
}
|
|
|
|
/* gray out deprecated transitions */
|
|
|
|
.deprecated > .transitionEdge,
|
|
|
|
.deprecated > .map {
|
|
|
|
opacity: 0.5;
|
|
|
|
}
|
|
|
|
.deprecated > .transition {
|
|
|
|
border-color: rgba(0, 0, 0, 0.5);
|
|
|
|
}
|
|
|
|
/* Show a border for all but the first transition */
|
|
|
|
.transition:nth-of-type(2),
|
|
|
|
.transition:nth-last-of-type(n+2) {
|
|
|
|
border-left: 2px solid;
|
|
|
|
margin-left: 0px;
|
|
|
|
}
|
|
|
|
/* special case for 2 transitions */
|
|
|
|
.transition:nth-last-of-type(1) {
|
|
|
|
border-left: none;
|
|
|
|
}
|
|
|
|
/* topmost transitions are not related */
|
|
|
|
#transitionView > .transition {
|
|
|
|
border-left: none;
|
|
|
|
}
|
|
|
|
/* topmost transition edge needs initial offset to be aligned */
|
|
|
|
#transitionView > .transition > .transitionEdge {
|
|
|
|
margin-left: 13px;
|
|
|
|
}
|
|
|
|
.transitionEdge {
|
|
|
|
height: 2px;
|
|
|
|
width: 80px;
|
|
|
|
display: inline-block;
|
|
|
|
margin: 0 0 2px 0;
|
|
|
|
background-color: black;
|
|
|
|
vertical-align: top;
|
|
|
|
padding-left: 15px;
|
|
|
|
}
|
|
|
|
.transitionLabel {
|
|
|
|
color: black;
|
|
|
|
transform: rotate(-15deg);
|
|
|
|
transform-origin: top left;
|
|
|
|
margin-top: -10px;
|
|
|
|
font-size: 10px;
|
|
|
|
white-space: normal;
|
|
|
|
word-break: break-all;
|
|
|
|
background-color: rgba(255,255,255,0.5);
|
|
|
|
}
|
2018-11-20 21:59:46 +00:00
|
|
|
.black{
|
|
|
|
background-color: black;
|
|
|
|
}
|
2017-12-12 13:35:33 +00:00
|
|
|
.red {
|
|
|
|
background-color: red;
|
|
|
|
}
|
|
|
|
.green {
|
|
|
|
background-color: green;
|
|
|
|
}
|
|
|
|
.yellow {
|
|
|
|
background-color: yellow;
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
.blue {
|
|
|
|
background-color: blue;
|
|
|
|
}
|
|
|
|
.orange {
|
|
|
|
background-color: orange;
|
|
|
|
}
|
|
|
|
.violet {
|
|
|
|
background-color: violet;
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
.showSubtransitions {
|
|
|
|
width: 0;
|
|
|
|
height: 0;
|
|
|
|
border-left: 6px solid transparent;
|
|
|
|
border-right: 6px solid transparent;
|
|
|
|
border-top: 10px solid black;
|
|
|
|
cursor: zoom-in;
|
|
|
|
margin: 4px 0 0 4px;
|
|
|
|
}
|
|
|
|
.showSubtransitions.opened {
|
|
|
|
border-top: none;
|
|
|
|
border-bottom: 10px solid black;
|
|
|
|
cursor: zoom-out;
|
|
|
|
}
|
|
|
|
#tooltip {
|
|
|
|
position: absolute;
|
|
|
|
width: 10px;
|
|
|
|
height: 10px;
|
|
|
|
background-color: red;
|
|
|
|
pointer-events: none;
|
|
|
|
z-index: 100;
|
|
|
|
display: none;
|
|
|
|
}
|
2020-06-18 09:47:13 +00:00
|
|
|
#searchBarInput {
|
|
|
|
width: 200px;
|
|
|
|
}
|
|
|
|
|
2017-12-12 13:35:33 +00:00
|
|
|
</style>
|
2020-09-28 13:50:56 +00:00
|
|
|
<script type="module" src="./map-processor.js"></script>
|
2017-12-12 13:35:33 +00:00
|
|
|
<script>
|
|
|
|
"use strict"
|
|
|
|
// =========================================================================
|
|
|
|
const kChunkHeight = 250;
|
|
|
|
const kChunkWidth = 10;
|
|
|
|
|
|
|
|
class State {
|
|
|
|
constructor() {
|
|
|
|
this._nofChunks = 400;
|
|
|
|
this._map = undefined;
|
|
|
|
this._timeline = undefined;
|
|
|
|
this._chunks = undefined;
|
|
|
|
this._view = new View(this);
|
|
|
|
this._navigation = new Navigation(this, this.view);
|
|
|
|
}
|
|
|
|
get timeline() { return this._timeline }
|
|
|
|
set timeline(value) {
|
|
|
|
this._timeline = value;
|
|
|
|
this.updateChunks();
|
|
|
|
this.view.updateTimeline();
|
|
|
|
this.view.updateStats();
|
|
|
|
}
|
|
|
|
get chunks() { return this._chunks }
|
|
|
|
get nofChunks() { return this._nofChunks }
|
|
|
|
set nofChunks(count) {
|
|
|
|
this._nofChunks = count;
|
|
|
|
this.updateChunks();
|
|
|
|
this.view.updateTimeline();
|
|
|
|
}
|
|
|
|
get view() { return this._view }
|
|
|
|
get navigation() { return this._navigation }
|
|
|
|
get map() { return this._map }
|
|
|
|
set map(value) {
|
|
|
|
this._map = value;
|
|
|
|
this._navigation.updateUrl();
|
|
|
|
this.view.updateMapDetails();
|
|
|
|
this.view.redraw();
|
|
|
|
}
|
|
|
|
updateChunks() {
|
|
|
|
this._chunks = this._timeline.chunks(this._nofChunks);
|
|
|
|
}
|
|
|
|
get entries() {
|
|
|
|
if (!this.map) return {};
|
|
|
|
return {
|
|
|
|
map: this.map.id,
|
|
|
|
time: this.map.time
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
// DOM Helper
|
|
|
|
function $(id) {
|
|
|
|
return document.getElementById(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeAllChildren(node) {
|
|
|
|
while (node.lastChild) {
|
|
|
|
node.removeChild(node.lastChild);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function selectOption(select, match) {
|
|
|
|
let options = select.options;
|
|
|
|
for (let i = 0; i < options.length; i++) {
|
|
|
|
if (match(i, options[i])) {
|
|
|
|
select.selectedIndex = i;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function div(classes) {
|
|
|
|
let node = document.createElement('div');
|
|
|
|
if (classes !== void 0) {
|
2018-11-20 21:59:46 +00:00
|
|
|
if (typeof classes === "string") {
|
2017-12-12 13:35:33 +00:00
|
|
|
node.classList.add(classes);
|
|
|
|
} else {
|
|
|
|
classes.forEach(cls => node.classList.add(cls));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
function table(className) {
|
|
|
|
let node = document.createElement("table")
|
|
|
|
if (className) node.classList.add(className)
|
|
|
|
return node;
|
|
|
|
}
|
2018-11-20 21:59:46 +00:00
|
|
|
|
|
|
|
function td(textOrNode) {
|
2017-12-12 13:35:33 +00:00
|
|
|
let node = document.createElement("td");
|
2018-11-20 21:59:46 +00:00
|
|
|
if (typeof textOrNode === "object") {
|
|
|
|
node.appendChild(textOrNode);
|
|
|
|
} else {
|
|
|
|
node.innerText = textOrNode;
|
|
|
|
}
|
2017-12-12 13:35:33 +00:00
|
|
|
return node;
|
|
|
|
}
|
2018-11-20 21:59:46 +00:00
|
|
|
|
2020-02-26 16:43:14 +00:00
|
|
|
|
2017-12-12 13:35:33 +00:00
|
|
|
function tr() {
|
2020-02-26 16:43:14 +00:00
|
|
|
return document.createElement("tr");
|
2017-12-12 13:35:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
define(Array.prototype, "histogram", function(mapFn) {
|
|
|
|
let histogram = [];
|
|
|
|
for (let i = 0; i < this.length; i++) {
|
|
|
|
let value = this[i];
|
|
|
|
let index = Math.round(mapFn(value))
|
|
|
|
let bucket = histogram[index];
|
|
|
|
if (bucket !== undefined) {
|
|
|
|
bucket.push(value);
|
|
|
|
} else {
|
|
|
|
histogram[index] = [value];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (let i = 0; i < histogram.length; i++) {
|
|
|
|
histogram[i] = histogram[i] || [];
|
|
|
|
}
|
|
|
|
return histogram;
|
|
|
|
});
|
|
|
|
|
2020-06-18 09:47:13 +00:00
|
|
|
|
2017-12-12 13:35:33 +00:00
|
|
|
// =========================================================================
|
|
|
|
// EventHandlers
|
2020-06-18 09:47:13 +00:00
|
|
|
function handleSearchBar(){
|
|
|
|
let searchBar = $('searchBarInput');
|
|
|
|
let searchBarInput = searchBar.value;
|
|
|
|
let selectedMap = V8Map.get(searchBarInput);
|
|
|
|
//removeAllChildren($('mapIdList'));
|
|
|
|
if(selectedMap){
|
|
|
|
let map = selectedMap;
|
|
|
|
document.state.map = map;
|
|
|
|
searchBar.className = "green";
|
|
|
|
} else {
|
|
|
|
searchBar.className = "red";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-12 13:35:33 +00:00
|
|
|
function handleBodyLoad() {
|
2018-11-20 21:59:46 +00:00
|
|
|
let upload = $('fileReader');
|
|
|
|
upload.onclick = (e) => $("file").click();
|
|
|
|
upload.ondragover = (e) => e.preventDefault();
|
|
|
|
upload.ondrop = (e) => handleLoadFile(e);
|
|
|
|
$('file').onchange = (e) => handleLoadFile(e);
|
|
|
|
upload.onkeydown = (e) => {
|
|
|
|
if (event.key == "Enter") $("file").click();
|
|
|
|
};
|
2017-12-12 13:35:33 +00:00
|
|
|
upload.focus();
|
|
|
|
|
|
|
|
document.state = new State();
|
|
|
|
$("transitionView").addEventListener("mousemove", e => {
|
|
|
|
let tooltip = $("tooltip");
|
|
|
|
tooltip.style.left = e.pageX + "px";
|
|
|
|
tooltip.style.top = e.pageY + "px";
|
|
|
|
let map = e.target.map;
|
|
|
|
if (map) {
|
2018-11-20 21:59:46 +00:00
|
|
|
$("tooltipContents").innerText = map.description;
|
2017-12-12 13:35:33 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-11-20 21:59:46 +00:00
|
|
|
function handleLoadFile(event) {
|
|
|
|
// Used for drop and file change.
|
|
|
|
event.preventDefault();
|
|
|
|
let host = event.dataTransfer ? event.dataTransfer : event.target;
|
|
|
|
let file = host.files[0];
|
|
|
|
let reader = new FileReader();
|
|
|
|
document.body.className = 'loading';
|
|
|
|
reader.onload = function(evt) {
|
|
|
|
try {
|
|
|
|
handleLoadText(this.result);
|
|
|
|
document.body.className = 'success';
|
|
|
|
} catch(e) {
|
|
|
|
document.body.className = 'failure';
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Defer the reading to allow spinner CSS animation.
|
|
|
|
setTimeout(() => reader.readAsText(file), 0);
|
2017-12-12 13:35:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-20 21:59:46 +00:00
|
|
|
|
2017-12-12 13:35:33 +00:00
|
|
|
function handleLoadText(text) {
|
|
|
|
let mapProcessor = new MapProcessor();
|
|
|
|
document.state.timeline = mapProcessor.processString(text);
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleKeyDown(event) {
|
|
|
|
let nav = document.state.navigation;
|
|
|
|
switch(event.key) {
|
|
|
|
case "ArrowUp":
|
|
|
|
event.preventDefault();
|
|
|
|
if (event.shiftKey) {
|
|
|
|
nav.selectPrevEdge();
|
|
|
|
} else {
|
|
|
|
nav.moveInChunk(-1);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
case "ArrowDown":
|
|
|
|
event.preventDefault();
|
|
|
|
if (event.shiftKey) {
|
|
|
|
nav.selectNextEdge();
|
|
|
|
} else {
|
|
|
|
nav.moveInChunk(1);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
case "ArrowLeft":
|
|
|
|
nav.moveInChunks(false);
|
|
|
|
break;
|
|
|
|
case "ArrowRight":
|
|
|
|
nav.moveInChunks(true);
|
|
|
|
break;
|
|
|
|
case "+":
|
|
|
|
nav.increaseTimelineResolution();
|
|
|
|
break;
|
|
|
|
case "-":
|
|
|
|
nav.decreaseTimelineResolution();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
document.onkeydown = handleKeyDown;
|
|
|
|
|
|
|
|
function handleTimelineIndicatorMove(event) {
|
|
|
|
if (event.buttons == 0) return;
|
|
|
|
let timelineTotalWidth = $("timelineCanvas").offsetWidth;
|
|
|
|
let factor = $("timelineOverview").offsetWidth / timelineTotalWidth;
|
|
|
|
$("timeline").scrollLeft += event.movementX / factor;
|
|
|
|
}
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
Object.defineProperty(Edge.prototype, 'getColor', { value:function() {
|
|
|
|
return transitionTypeToColor(this.type);
|
|
|
|
}});
|
|
|
|
|
|
|
|
class Navigation {
|
|
|
|
constructor(state, view) {
|
|
|
|
this.state = state;
|
|
|
|
this.view = view;
|
|
|
|
}
|
|
|
|
get map() { return this.state.map }
|
|
|
|
set map(value) { this.state.map = value }
|
|
|
|
get chunks() { return this.state.chunks }
|
|
|
|
|
|
|
|
increaseTimelineResolution() {
|
|
|
|
this.state.nofChunks *= 1.5;
|
|
|
|
}
|
|
|
|
|
|
|
|
decreaseTimelineResolution() {
|
|
|
|
this.state.nofChunks /= 1.5;
|
|
|
|
}
|
|
|
|
|
|
|
|
selectNextEdge() {
|
|
|
|
if (!this.map) return;
|
|
|
|
if (this.map.children.length != 1) return;
|
|
|
|
this.map = this.map.children[0].to;
|
|
|
|
}
|
|
|
|
|
|
|
|
selectPrevEdge() {
|
|
|
|
if (!this.map) return;
|
|
|
|
if (!this.map.parent()) return;
|
|
|
|
this.map = this.map.parent();
|
|
|
|
}
|
|
|
|
|
|
|
|
selectDefaultMap() {
|
|
|
|
this.map = this.chunks[0].at(0);
|
|
|
|
}
|
|
|
|
moveInChunks(next) {
|
|
|
|
if (!this.map) return this.selectDefaultMap();
|
|
|
|
let chunkIndex = this.map.chunkIndex(this.chunks);
|
|
|
|
let chunk = this.chunks[chunkIndex];
|
|
|
|
let index = chunk.indexOf(this.map);
|
|
|
|
if (next) {
|
|
|
|
chunk = chunk.next(this.chunks);
|
|
|
|
} else {
|
|
|
|
chunk = chunk.prev(this.chunks);
|
|
|
|
}
|
|
|
|
if (!chunk) return;
|
|
|
|
index = Math.min(index, chunk.size()-1);
|
|
|
|
this.map = chunk.at(index);
|
|
|
|
}
|
|
|
|
|
|
|
|
moveInChunk(delta) {
|
|
|
|
if (!this.map) return this.selectDefaultMap();
|
|
|
|
let chunkIndex = this.map.chunkIndex(this.chunks)
|
|
|
|
let chunk = this.chunks[chunkIndex];
|
|
|
|
let index = chunk.indexOf(this.map) + delta;
|
|
|
|
let map;
|
|
|
|
if (index < 0) {
|
|
|
|
map = chunk.prev(this.chunks).last();
|
|
|
|
} else if (index >= chunk.size()) {
|
|
|
|
map = chunk.next(this.chunks).first()
|
|
|
|
} else {
|
|
|
|
map = chunk.at(index);
|
|
|
|
}
|
|
|
|
this.map = map;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateUrl() {
|
|
|
|
let entries = this.state.entries;
|
|
|
|
let params = new URLSearchParams(entries);
|
|
|
|
window.history.pushState(entries, "", "?" + params.toString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class View {
|
|
|
|
constructor(state) {
|
|
|
|
this.state = state;
|
|
|
|
setInterval(this.updateOverviewWindow, 50);
|
|
|
|
this.backgroundCanvas = document.createElement("canvas");
|
|
|
|
this.transitionView = new TransitionView(state, $("transitionView"));
|
|
|
|
this.statsView = new StatsView(state, $("stats"));
|
|
|
|
this.isLocked = false;
|
|
|
|
}
|
|
|
|
get chunks() { return this.state.chunks }
|
|
|
|
get timeline() { return this.state.timeline }
|
|
|
|
get map() { return this.state.map }
|
|
|
|
|
|
|
|
updateStats() {
|
|
|
|
this.statsView.update();
|
|
|
|
}
|
|
|
|
|
|
|
|
updateMapDetails() {
|
|
|
|
let details = "";
|
|
|
|
if (this.map) {
|
|
|
|
details += "ID: " + this.map.id;
|
2018-11-29 10:44:15 +00:00
|
|
|
details += "\nSource location: " + this.map.filePosition;
|
2017-12-12 13:35:33 +00:00
|
|
|
details += "\n" + this.map.description;
|
|
|
|
}
|
|
|
|
$("mapDetails").innerText = details;
|
|
|
|
this.transitionView.showMap(this.map);
|
|
|
|
}
|
|
|
|
|
|
|
|
updateTimeline() {
|
|
|
|
let chunksNode = $("timelineChunks");
|
|
|
|
removeAllChildren(chunksNode);
|
|
|
|
let chunks = this.chunks;
|
|
|
|
let max = chunks.max(each => each.size());
|
|
|
|
let start = this.timeline.startTime;
|
|
|
|
let end = this.timeline.endTime;
|
|
|
|
let duration = end - start;
|
|
|
|
const timeToPixel = chunks.length * kChunkWidth / duration;
|
|
|
|
let addTimestamp = (time, name) => {
|
|
|
|
let timeNode = div("timestamp");
|
|
|
|
timeNode.innerText = name;
|
|
|
|
timeNode.style.left = ((time-start) * timeToPixel) + "px";
|
|
|
|
chunksNode.appendChild(timeNode);
|
|
|
|
};
|
2020-04-01 09:46:41 +00:00
|
|
|
let backgroundTodo = [];
|
2017-12-12 13:35:33 +00:00
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
|
|
let chunk = chunks[i];
|
|
|
|
let height = (chunk.size() / max * kChunkHeight);
|
|
|
|
chunk.height = height;
|
|
|
|
if (chunk.isEmpty()) continue;
|
|
|
|
let node = div();
|
|
|
|
node.className = "chunk";
|
|
|
|
node.style.left = (i * kChunkWidth) + "px";
|
|
|
|
node.style.height = height + "px";
|
|
|
|
node.chunk = chunk;
|
|
|
|
node.addEventListener("mousemove", e => this.handleChunkMouseMove(e));
|
|
|
|
node.addEventListener("click", e => this.handleChunkClick(e));
|
|
|
|
node.addEventListener("dblclick", e => this.handleChunkDoubleClick(e));
|
2020-04-01 09:46:41 +00:00
|
|
|
backgroundTodo.push([chunk, node])
|
2017-12-12 13:35:33 +00:00
|
|
|
chunksNode.appendChild(node);
|
|
|
|
chunk.markers.forEach(marker => addTimestamp(marker.time, marker.name));
|
|
|
|
}
|
2020-04-01 09:46:41 +00:00
|
|
|
|
|
|
|
this.asyncSetTimelineChunkBackground(backgroundTodo)
|
|
|
|
|
2017-12-12 13:35:33 +00:00
|
|
|
// Put a time marker roughly every 20 chunks.
|
|
|
|
let expected = duration / chunks.length * 20;
|
|
|
|
let interval = (10 ** Math.floor(Math.log10(expected)));
|
|
|
|
let correction = Math.log10(expected / interval);
|
|
|
|
correction = (correction < 0.33) ? 1 : (correction < 0.75) ? 2.5 : 5;
|
|
|
|
interval *= correction;
|
|
|
|
|
|
|
|
let time = start;
|
|
|
|
while (time < end) {
|
|
|
|
addTimestamp(time, ((time-start) / 1000) + " ms");
|
|
|
|
time += interval;
|
|
|
|
}
|
|
|
|
this.drawOverview();
|
|
|
|
this.redraw();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleChunkMouseMove(event) {
|
|
|
|
if (this.isLocked) return false;
|
|
|
|
let chunk = event.target.chunk;
|
|
|
|
if (!chunk) return;
|
|
|
|
// topmost map (at chunk.height) == map #0.
|
|
|
|
let relativeIndex =
|
|
|
|
Math.round(event.layerY / event.target.offsetHeight * chunk.size());
|
|
|
|
let map = chunk.at(relativeIndex);
|
|
|
|
this.state.map = map;
|
|
|
|
}
|
|
|
|
|
|
|
|
handleChunkClick(event) {
|
|
|
|
this.isLocked = !this.isLocked;
|
|
|
|
}
|
|
|
|
|
|
|
|
handleChunkDoubleClick(event) {
|
|
|
|
this.isLocked = true;
|
|
|
|
let chunk = event.target.chunk;
|
|
|
|
if (!chunk) return;
|
|
|
|
this.transitionView.showMaps(chunk.getUniqueTransitions());
|
|
|
|
}
|
|
|
|
|
2020-04-01 09:46:41 +00:00
|
|
|
asyncSetTimelineChunkBackground(backgroundTodo) {
|
|
|
|
const kIncrement = 100;
|
|
|
|
let start = 0;
|
|
|
|
let delay = 1;
|
|
|
|
while (start < backgroundTodo.length) {
|
|
|
|
let end = Math.min(start+kIncrement, backgroundTodo.length);
|
|
|
|
setTimeout((from, to) => {
|
|
|
|
for (let i = from; i < to; i++) {
|
|
|
|
let [chunk, node] = backgroundTodo[i];
|
|
|
|
this.setTimelineChunkBackground(chunk, node);
|
|
|
|
}
|
|
|
|
}, delay++, start, end);
|
|
|
|
start = end;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-12 13:35:33 +00:00
|
|
|
setTimelineChunkBackground(chunk, node) {
|
|
|
|
// Render the types of transitions as bar charts
|
|
|
|
const kHeight = chunk.height;
|
|
|
|
const kWidth = 1;
|
|
|
|
this.backgroundCanvas.width = kWidth;
|
|
|
|
this.backgroundCanvas.height = kHeight;
|
|
|
|
let ctx = this.backgroundCanvas.getContext("2d");
|
|
|
|
ctx.clearRect(0, 0, kWidth, kHeight);
|
|
|
|
let y = 0;
|
|
|
|
let total = chunk.size();
|
|
|
|
let type, count;
|
|
|
|
if (true) {
|
|
|
|
chunk.getTransitionBreakdown().forEach(([type, count]) => {
|
|
|
|
ctx.fillStyle = transitionTypeToColor(type);
|
|
|
|
let height = count / total * kHeight;
|
|
|
|
ctx.fillRect(0, y, kWidth, y + height);
|
|
|
|
y += height;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
chunk.items.forEach(map => {
|
|
|
|
ctx.fillStyle = transitionTypeToColor(map.getType());
|
|
|
|
let y = chunk.yOffset(map);
|
|
|
|
ctx.fillRect(0, y, kWidth, y + 1);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-01 09:46:41 +00:00
|
|
|
let imageData = this.backgroundCanvas.toDataURL("image/webp", 0.2);
|
2017-12-12 13:35:33 +00:00
|
|
|
node.style.backgroundImage = "url(" + imageData + ")";
|
|
|
|
}
|
|
|
|
|
|
|
|
updateOverviewWindow() {
|
|
|
|
let indicator = $("timelineOverviewIndicator");
|
|
|
|
let totalIndicatorWidth = $("timelineOverview").offsetWidth;
|
|
|
|
let div = $("timeline");
|
|
|
|
let timelineTotalWidth = $("timelineCanvas").offsetWidth;
|
|
|
|
let factor = $("timelineOverview").offsetWidth / timelineTotalWidth;
|
|
|
|
let width = div.offsetWidth * factor;
|
|
|
|
let left = div.scrollLeft * factor;
|
|
|
|
indicator.style.width = width + "px";
|
|
|
|
indicator.style.left = left + "px";
|
|
|
|
}
|
|
|
|
|
|
|
|
drawOverview() {
|
|
|
|
const height = 50;
|
|
|
|
const kFactor = 2;
|
|
|
|
let canvas = this.backgroundCanvas;
|
|
|
|
canvas.height = height;
|
|
|
|
canvas.width = window.innerWidth;
|
|
|
|
let ctx = canvas.getContext("2d");
|
|
|
|
|
|
|
|
let chunks = this.state.timeline.chunkSizes(canvas.width * kFactor);
|
|
|
|
let max = chunks.max();
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, height);
|
|
|
|
ctx.strokeStyle = "black";
|
|
|
|
ctx.fillStyle = "black";
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.moveTo(0,height);
|
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
|
|
ctx.lineTo(i/kFactor, height - chunks[i]/max * height);
|
|
|
|
}
|
|
|
|
ctx.lineTo(chunks.length, height);
|
|
|
|
ctx.stroke();
|
|
|
|
ctx.closePath();
|
|
|
|
ctx.fill();
|
2020-04-01 09:46:41 +00:00
|
|
|
let imageData = canvas.toDataURL("image/webp", 0.2);
|
2017-12-12 13:35:33 +00:00
|
|
|
$("timelineOverview").style.backgroundImage = "url(" + imageData + ")";
|
|
|
|
}
|
|
|
|
|
|
|
|
redraw() {
|
|
|
|
let canvas= $("timelineCanvas");
|
|
|
|
canvas.width = (this.chunks.length+1) * kChunkWidth;
|
|
|
|
canvas.height = kChunkHeight;
|
|
|
|
let ctx = canvas.getContext("2d");
|
|
|
|
ctx.clearRect(0, 0, canvas.width, kChunkHeight);
|
|
|
|
if (!this.state.map) return;
|
|
|
|
this.drawEdges(ctx);
|
|
|
|
}
|
|
|
|
|
|
|
|
setMapStyle(map, ctx) {
|
|
|
|
ctx.fillStyle = map.edge && map.edge.from ? "black" : "green";
|
|
|
|
}
|
|
|
|
|
|
|
|
setEdgeStyle(edge, ctx) {
|
|
|
|
let color = edge.getColor();
|
|
|
|
ctx.strokeStyle = color;
|
|
|
|
ctx.fillStyle = color;
|
|
|
|
}
|
|
|
|
|
|
|
|
markMap(ctx, map) {
|
|
|
|
let [x, y] = map.position(this.state.chunks);
|
|
|
|
ctx.beginPath();
|
|
|
|
this.setMapStyle(map, ctx);
|
|
|
|
ctx.arc(x, y, 3, 0, 2 * Math.PI);
|
|
|
|
ctx.fill();
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.fillStyle = "white";
|
|
|
|
ctx.arc(x, y, 2, 0, 2 * Math.PI);
|
|
|
|
ctx.fill();
|
|
|
|
}
|
|
|
|
|
|
|
|
markSelectedMap(ctx, map) {
|
|
|
|
let [x, y] = map.position(this.state.chunks);
|
|
|
|
ctx.beginPath();
|
|
|
|
this.setMapStyle(map, ctx);
|
|
|
|
ctx.arc(x, y, 6, 0, 2 * Math.PI);
|
|
|
|
ctx.stroke();
|
|
|
|
}
|
|
|
|
|
|
|
|
drawEdges(ctx) {
|
|
|
|
// Draw the trace of maps in reverse order to make sure the outgoing
|
|
|
|
// transitions of previous maps aren't drawn over.
|
|
|
|
const kMaxOutgoingEdges = 100;
|
|
|
|
let nofEdges = 0;
|
|
|
|
let stack = [];
|
|
|
|
let current = this.state.map;
|
|
|
|
while (current && nofEdges < kMaxOutgoingEdges) {
|
|
|
|
nofEdges += current.children.length;
|
|
|
|
stack.push(current);
|
|
|
|
current = current.parent();
|
|
|
|
}
|
|
|
|
ctx.save();
|
|
|
|
this.drawOutgoingEdges(ctx, this.state.map, 3);
|
|
|
|
ctx.restore();
|
|
|
|
|
|
|
|
let labelOffset = 15;
|
|
|
|
let xPrev = 0;
|
|
|
|
while (current = stack.pop()) {
|
|
|
|
if (current.edge) {
|
|
|
|
this.setEdgeStyle(current.edge, ctx);
|
|
|
|
let [xTo, yTo] = this.drawEdge(ctx, current.edge, true, labelOffset);
|
|
|
|
if (xTo == xPrev) {
|
|
|
|
labelOffset += 8;
|
|
|
|
} else {
|
|
|
|
labelOffset = 15
|
|
|
|
}
|
|
|
|
xPrev = xTo;
|
|
|
|
}
|
|
|
|
this.markMap(ctx, current);
|
|
|
|
current = current.parent();
|
|
|
|
ctx.save();
|
|
|
|
// this.drawOutgoingEdges(ctx, current, 1);
|
|
|
|
ctx.restore();
|
|
|
|
}
|
|
|
|
// Mark selected map
|
|
|
|
this.markSelectedMap(ctx, this.state.map);
|
|
|
|
}
|
|
|
|
|
|
|
|
drawEdge(ctx, edge, showLabel=true, labelOffset=20) {
|
|
|
|
if (!edge.from || !edge.to) return [-1, -1];
|
|
|
|
let [xFrom, yFrom] = edge.from.position(this.chunks);
|
|
|
|
let [xTo, yTo] = edge.to.position(this.chunks);
|
|
|
|
let sameChunk = xTo == xFrom;
|
|
|
|
if (sameChunk) labelOffset += 8;
|
|
|
|
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.moveTo(xFrom, yFrom);
|
|
|
|
let offsetX = 20;
|
|
|
|
let offsetY = 20;
|
|
|
|
let midX = xFrom + (xTo- xFrom) / 2;
|
|
|
|
let midY = (yFrom + yTo) / 2 - 100;
|
|
|
|
if (!sameChunk) {
|
|
|
|
ctx.quadraticCurveTo(midX, midY, xTo, yTo);
|
|
|
|
} else {
|
|
|
|
ctx.lineTo(xTo, yTo);
|
|
|
|
}
|
|
|
|
if (!showLabel) {
|
|
|
|
ctx.stroke();
|
|
|
|
} else {
|
|
|
|
let centerX, centerY;
|
|
|
|
if (!sameChunk) {
|
|
|
|
centerX = (xFrom/2 + midX + xTo/2)/2;
|
|
|
|
centerY = (yFrom/2 + midY + yTo/2)/2;
|
|
|
|
} else {
|
|
|
|
centerX = xTo;
|
|
|
|
centerY = yTo;
|
|
|
|
}
|
|
|
|
ctx.moveTo(centerX, centerY);
|
|
|
|
ctx.lineTo(centerX + offsetX, centerY - labelOffset);
|
|
|
|
ctx.stroke();
|
|
|
|
ctx.textAlign = "left";
|
|
|
|
ctx.fillText(edge.toString(), centerX + offsetX + 2, centerY - labelOffset)
|
|
|
|
}
|
|
|
|
return [xTo, yTo];
|
|
|
|
}
|
|
|
|
|
|
|
|
drawOutgoingEdges(ctx, map, max=10, depth=0) {
|
|
|
|
if (!map) return;
|
|
|
|
if (depth >= max) return;
|
|
|
|
ctx.globalAlpha = 0.5 - depth * (0.3/max);
|
|
|
|
ctx.strokeStyle = "#666";
|
|
|
|
|
|
|
|
const limit = Math.min(map.children.length, 100)
|
|
|
|
for (let i = 0; i < limit; i++) {
|
|
|
|
let edge = map.children[i];
|
|
|
|
this.drawEdge(ctx, edge, true);
|
|
|
|
this.drawOutgoingEdges(ctx, edge.to, max, depth+1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class TransitionView {
|
|
|
|
constructor(state, node) {
|
|
|
|
this.state = state;
|
|
|
|
this.container = node;
|
|
|
|
this.currentNode = node;
|
|
|
|
this.currentMap = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
selectMap(map) {
|
|
|
|
this.currentMap = map;
|
|
|
|
this.state.map = map;
|
|
|
|
}
|
|
|
|
|
|
|
|
showMap(map) {
|
|
|
|
if (this.currentMap === map) return;
|
|
|
|
this.currentMap = map;
|
|
|
|
this._showMaps([map]);
|
|
|
|
}
|
|
|
|
|
|
|
|
showMaps(list, name) {
|
|
|
|
this.state.view.isLocked = true;
|
|
|
|
this._showMaps(list);
|
|
|
|
}
|
|
|
|
|
|
|
|
_showMaps(list, name) {
|
|
|
|
// Hide the container to avoid any layouts.
|
|
|
|
this.container.style.display = "none";
|
|
|
|
removeAllChildren(this.container);
|
|
|
|
list.forEach(map => this.addMapAndParentTransitions(map));
|
|
|
|
this.container.style.display = ""
|
|
|
|
}
|
|
|
|
|
|
|
|
addMapAndParentTransitions(map) {
|
|
|
|
if (map === void 0) return;
|
|
|
|
this.currentNode = this.container;
|
|
|
|
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 = div("map");
|
|
|
|
if (map.edge) node.classList.add(map.edge.getColor());
|
|
|
|
node.map = map;
|
|
|
|
node.addEventListener("click", () => this.selectMap(map));
|
|
|
|
if (map.children.length > 1) {
|
|
|
|
node.innerText = map.children.length;
|
|
|
|
let showSubtree = 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", map.edge.getColor()];
|
|
|
|
let edge = div(classes);
|
|
|
|
let labelNode = div("transitionLabel");
|
|
|
|
labelNode.innerText = map.edge.toString();
|
|
|
|
edge.appendChild(labelNode);
|
|
|
|
return edge;
|
|
|
|
}
|
|
|
|
|
|
|
|
addTransitionTo(map) {
|
|
|
|
// transition[ transitions[ transition[...], transition[...], ...]];
|
|
|
|
|
|
|
|
let transition = 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 = 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]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class StatsView {
|
|
|
|
constructor(state, node) {
|
|
|
|
this.state = state;
|
|
|
|
this.node = node;
|
|
|
|
}
|
|
|
|
get timeline() { return this.state.timeline }
|
|
|
|
get transitionView() { return this.state.view.transitionView; }
|
|
|
|
update() {
|
|
|
|
removeAllChildren(this.node);
|
|
|
|
this.updateGeneralStats();
|
|
|
|
this.updateNamedTransitionsStats();
|
|
|
|
}
|
|
|
|
updateGeneralStats() {
|
|
|
|
let pairs = [
|
2020-04-08 20:41:02 +00:00
|
|
|
["Total", null, e => true],
|
2018-11-20 21:59:46 +00:00
|
|
|
["Transitions", 'black', e => e.edge && e.edge.isTransition()],
|
|
|
|
["Fast to Slow", 'violet', e => e.edge && e.edge.isFastToSlow()],
|
|
|
|
["Slow to Fast", 'orange', e => e.edge && e.edge.isSlowToFast()],
|
|
|
|
["Initial Map", 'yellow', e => e.edge && e.edge.isInitial()],
|
|
|
|
["Replace Descriptors", 'red', e => e.edge && e.edge.isReplaceDescriptors()],
|
|
|
|
["Copy as Prototype", 'red', e => e.edge && e.edge.isCopyAsPrototype()],
|
|
|
|
["Optimize as Prototype", null, e => e.edge && e.edge.isOptimizeAsPrototype()],
|
|
|
|
["Deprecated", null, e => e.isDeprecated()],
|
|
|
|
["Bootstrapped", 'green', e => e.isBootstrapped()],
|
2017-12-12 13:35:33 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
let text = "";
|
2020-02-26 16:43:14 +00:00
|
|
|
let tableNode = table("transitionType");
|
2020-04-08 20:41:02 +00:00
|
|
|
tableNode.innerHTML = "<thead><tr><td>Color</td><td>Type</td><td>Count</td><td>Percent</td></tr></thead>";
|
2017-12-12 13:35:33 +00:00
|
|
|
let name, filter;
|
|
|
|
let total = this.timeline.size();
|
2018-11-20 21:59:46 +00:00
|
|
|
pairs.forEach(([name, color, filter]) => {
|
2017-12-12 13:35:33 +00:00
|
|
|
let row = tr();
|
2018-11-20 21:59:46 +00:00
|
|
|
if (color !== null) {
|
|
|
|
row.appendChild(td(div(['colorbox', color])));
|
|
|
|
} else {
|
2020-04-01 09:46:41 +00:00
|
|
|
row.appendChild(td(""));
|
|
|
|
}
|
|
|
|
row.onclick = (e) => {
|
|
|
|
// lazily compute the stats
|
|
|
|
let node = e.target.parentNode;
|
|
|
|
if (node.maps == undefined) {
|
|
|
|
node.maps = this.timeline.filterUniqueTransitions(filter);
|
|
|
|
}
|
|
|
|
this.transitionView.showMaps(node.maps);
|
2018-11-20 21:59:46 +00:00
|
|
|
}
|
2017-12-12 13:35:33 +00:00
|
|
|
row.appendChild(td(name));
|
|
|
|
let count = this.timeline.count(filter);
|
|
|
|
row.appendChild(td(count));
|
|
|
|
let percent = Math.round(count / total * 1000) / 10;
|
2020-02-26 16:43:14 +00:00
|
|
|
row.appendChild(td(percent.toFixed(1) + "%"));
|
2017-12-12 13:35:33 +00:00
|
|
|
tableNode.appendChild(row);
|
|
|
|
});
|
|
|
|
this.node.appendChild(tableNode);
|
|
|
|
};
|
|
|
|
updateNamedTransitionsStats() {
|
|
|
|
let tableNode = table("transitionTable");
|
|
|
|
let nameMapPairs = Array.from(this.timeline.transitions.entries());
|
2020-04-08 20:41:02 +00:00
|
|
|
tableNode.innerHTML = "<thead><tr><td>Propery Name</td><td>#</td></tr></thead>";
|
2017-12-12 13:35:33 +00:00
|
|
|
nameMapPairs
|
|
|
|
.sort((a,b) => b[1].length - a[1].length)
|
|
|
|
.forEach(([name, maps]) => {
|
|
|
|
let row = tr();
|
|
|
|
row.maps = maps;
|
|
|
|
row.addEventListener("click",
|
|
|
|
e => this.transitionView.showMaps(
|
|
|
|
e.target.parentNode.maps.map(map => map.to)));
|
|
|
|
row.appendChild(td(name));
|
|
|
|
row.appendChild(td(maps.length));
|
|
|
|
tableNode.appendChild(row);
|
|
|
|
});
|
|
|
|
this.node.appendChild(tableNode);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
function transitionTypeToColor(type) {
|
|
|
|
switch(type) {
|
|
|
|
case "new": return "green";
|
|
|
|
case "Normalize": return "violet";
|
2018-11-20 21:59:46 +00:00
|
|
|
case "SlowToFast": return "orange";
|
2017-12-12 13:35:33 +00:00
|
|
|
case "InitialMap": return "yellow";
|
|
|
|
case "Transition": return "black";
|
|
|
|
case "ReplaceDescriptors": return "red";
|
|
|
|
}
|
|
|
|
return "black";
|
|
|
|
}
|
|
|
|
|
|
|
|
// ShadowDom elements =========================================================
|
|
|
|
|
|
|
|
</script>
|
|
|
|
</head>
|
|
|
|
<body onload="handleBodyLoad(event)" onkeypress="handleKeyDown(event)">
|
2018-11-20 21:59:46 +00:00
|
|
|
<h1>V8 Map Explorer</h1>
|
2017-12-12 13:35:33 +00:00
|
|
|
<section>
|
2018-11-20 21:59:46 +00:00
|
|
|
<div id="fileReader" tabindex=1 >
|
|
|
|
<span id="label">
|
|
|
|
Drag and drop a v8.log file into this area, or click to choose from disk.
|
|
|
|
</span>
|
2019-01-04 12:18:43 +00:00
|
|
|
<input id="file" type="file" name="files">
|
2018-11-20 21:59:46 +00:00
|
|
|
</div>
|
|
|
|
<div id="loader">
|
|
|
|
<div id="spinner"></div>
|
|
|
|
</div>
|
2017-12-12 13:35:33 +00:00
|
|
|
</section>
|
|
|
|
|
2018-11-20 21:59:46 +00:00
|
|
|
<div id="content">
|
|
|
|
<h2>Stats</h2>
|
|
|
|
<section id="stats"></section>
|
2017-12-12 13:35:33 +00:00
|
|
|
|
2018-11-20 21:59:46 +00:00
|
|
|
<h2>Timeline</h2>
|
|
|
|
<div id="timeline">
|
|
|
|
<div id="timelineLabel">Frequency</div>
|
2019-01-04 12:18:43 +00:00
|
|
|
<div id="timelineChunks"></div>
|
|
|
|
<canvas id="timelineCanvas"></canvas>
|
2018-11-20 21:59:46 +00:00
|
|
|
</div>
|
|
|
|
<div id="timelineOverview"
|
|
|
|
onmousemove="handleTimelineIndicatorMove(event)" >
|
|
|
|
<div id="timelineOverviewIndicator">
|
|
|
|
<div class="leftMask"></div>
|
|
|
|
<div class="rightMask"></div>
|
|
|
|
</div>
|
2017-12-12 13:35:33 +00:00
|
|
|
</div>
|
|
|
|
|
2018-11-20 21:59:46 +00:00
|
|
|
<h2>Transitions</h2>
|
|
|
|
<section id="transitionView"></section>
|
|
|
|
<br/>
|
2017-12-12 13:35:33 +00:00
|
|
|
|
2020-06-18 09:47:13 +00:00
|
|
|
|
|
|
|
<h2>Search Map by Address</h2>
|
|
|
|
<section id="searchBar"></section>
|
|
|
|
<input type="search" id="searchBarInput" placeholder="Search maps by address..">
|
|
|
|
<button onclick="handleSearchBar()">Search</button>
|
|
|
|
<ul id="mapIdList" title="Map Id List">
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
2018-11-20 21:59:46 +00:00
|
|
|
<h2>Selected Map</h2>
|
|
|
|
<section id="mapDetails"></section>
|
|
|
|
</div>
|
2017-12-12 13:35:33 +00:00
|
|
|
|
2018-11-20 21:59:46 +00:00
|
|
|
<section>
|
2020-04-08 20:41:02 +00:00
|
|
|
<h2>Instructions</h2>
|
2020-06-05 11:27:44 +00:00
|
|
|
<p>Visualize Map trees that have been gathered using <code>path/to/d8 $FILE --trace-maps</code>.</p>
|
|
|
|
<p>You can inspect the transition tree in DevTools by looking at <code>document.state.timeline.values</code>.
|
2020-04-08 20:41:02 +00:00
|
|
|
<h3>Keyboard Shortcuts</h3>
|
|
|
|
<dl>
|
|
|
|
<dt><kbd>SHIFT</kbd> + <kbd>Arrow Up</kbd></dt>
|
|
|
|
<dd>Follow Map transition forward (first child)</dd>
|
|
|
|
|
|
|
|
<dt><kbd>SHIFT</kbd> + <kbd>Arrow Down</kbd></dt>
|
|
|
|
<dd>Follow Map transition backwards</dd>
|
|
|
|
|
|
|
|
<dt><kbd>Arrow Up</kbd></dt>
|
|
|
|
<dd>Go to previous Map chunk</dd>
|
|
|
|
|
|
|
|
<dt><kbd>Arrow Down</kbd></dt>
|
|
|
|
<dd>Go to next Map in chunk</dd>
|
|
|
|
|
|
|
|
<dt><kbd>Arrow Left</kbd></dt>
|
|
|
|
<dd>Go to previous chunk</dd>
|
|
|
|
|
|
|
|
<dt><kbd>Arrow Right</kbd></dt>
|
|
|
|
<dd>Go to next chunk</dd>
|
|
|
|
|
|
|
|
<dt><kbd>+</kbd></dt>
|
|
|
|
<dd>Timeline zoom in</dd>
|
|
|
|
|
|
|
|
<dt><kbd>-</kbd></dt>
|
|
|
|
<dd>Timeline zoom out</dd>
|
|
|
|
</dl>
|
2018-11-20 21:59:46 +00:00
|
|
|
</section>
|
2017-12-12 13:35:33 +00:00
|
|
|
|
|
|
|
<div id="tooltip">
|
|
|
|
<div id="tooltipContents"></div>
|
|
|
|
</div>
|
|
|
|
</body>
|
|
|
|
</html>
|