blob: 5f3043796890cdbad194ba5973fe93b9a735a3e1 [file] [log] [blame]
<!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<md-bottom-bar class="bottom-nav">
<div class="md-layout md-alignment-left-center nav-content">
<div class="md-layout-item">
<md-toolbar
md-elevation="0"
class="md-transparent">
<div class="toolbar">
<div class="toolbar-content">
<div class="seek-time" v-if="seekTime">
<b>Seek time</b>: {{ seekTime }}
</div>
<div class="active-timeline" v-show="minimized">
<md-icon>
{{timelineActiveFile.type.icon}}
<md-tooltip md-direction="right">{{timelineActiveFile.type.name}}</md-tooltip>
</md-icon>
<timeline
:items="timelineActiveFile.timeline"
:selected-index="timelineActiveFile.selectedIndex"
:scale="scale"
@item-selected="onTimelineItemSelected($event, timelineActiveFileIndex)"
class="timeline"
/>
</div>
</div>
<md-button class="md-icon-button toggle-btn" @click="toggle">
<md-icon v-if="minimized">expand_less</md-icon>
<md-icon v-else>expand_more</md-icon>
</md-button>
</div>
</md-toolbar>
<div class="expanded-content" v-show="expanded">
<md-list>
<md-list-item v-for="(file, idx) in files" :key="file.filename">
<md-icon>
{{file.type.icon}}
<md-tooltip md-direction="right">{{file.type.name}}</md-tooltip>
</md-icon>
<timeline
:items="file.timeline"
:selected-index="file.selectedIndex"
:scale="scale"
@item-selected="onTimelineItemSelected($event, idx)"
class="timeline"
/>
</md-list-item>
</md-list>
<div class="options">
<div class="datafilter">
<label>Datafilter</label>
<datafilter v-for="file in files" :key="file.filename" :store="store" :file="file" />
</div>
</div>
</div>
</div>
</div>
</md-bottom-bar>
</template>
<script>
import Timeline from './Timeline.vue'
import DataFilter from './DataFilter.vue'
import { nanos_to_string } from './transform.js'
// Find the index of the last element matching the predicate in a sorted array
function findLastMatchingSorted(array, predicate) {
var a = 0;
var b = array.length - 1;
while (b - a > 1) {
var m = Math.floor((a + b) / 2);
if (predicate(array, m)) {
a = m;
} else {
b = m - 1;
}
}
return predicate(array, b) ? b : a;
}
export default {
name: 'bottom-navigation',
props: [ 'files', 'store', 'dataViewPositions', 'activeFile' ],
data() {
return {
minimized: true,
currentTimestamp: 0,
}
},
computed: {
expanded() {
return !this.minimized;
},
timelineActiveFile() {
if (this.activeFile) {
return this.activeFile;
}
if (!this.dataViewPositions) {
return this.files[0];
}
// If not active file is selected figure out which one takes up the most
// of the screen and mark that one as the active file
const visibleHeight =
Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
let maxScreenSpace = 0;
let selectedFile = this.files[0];
for (const file of this.files) {
const pos = this.dataViewPositions[file.filename];
let screenSpace = 0;
if (0 <= pos.top && pos.top <= visibleHeight) {
screenSpace = Math.min(visibleHeight, pos.bottom) - pos.top;
} else if (0 <= pos.bottom && pos.bottom <= visibleHeight) {
screenSpace = pos.bottom - Math.max(0, pos.top);
} else if (pos.top <=0 && pos.bottom >= visibleHeight) {
screenSpace = visibleHeight;
}
if (screenSpace >= maxScreenSpace) {
maxScreenSpace = screenSpace;
selectedFile = file;
}
}
return selectedFile;
},
timelineActiveFileIndex() {
for (let i = 0; i < this.files.length; i++) {
if (this.files[i].filename == this.timelineActiveFile.filename) {
return i;
}
}
throw "Active file index not found";
},
seekTime() {
return nanos_to_string(this.currentTimestamp);
},
scale() {
var mx = Math.max(...(this.files.map(f => Math.max(...f.timeline))));
var mi = Math.min(...(this.files.map(f => Math.min(...f.timeline))));
return [mi, mx];
},
},
methods: {
toggle() {
this.minimized = !this.minimized;
},
fileIsVisible(f) {
return this.visibleDataViews.includes(f.filename);
},
onTimelineItemSelected(index, timelineIndex) {
this.files[timelineIndex].selectedIndex = index;
var t = parseInt(this.files[timelineIndex].timeline[index]);
for (var i = 0; i < this.files.length; i++) {
if (i != timelineIndex) {
this.files[i].selectedIndex = findLastMatchingSorted(this.files[i].timeline, function(array, idx) {
return parseInt(array[idx]) <= t;
});
}
}
this.currentTimestamp = t;
},
advanceTimeline(direction) {
var closestTimeline = -1;
var timeDiff = Infinity;
for (var idx = 0; idx < this.files.length; idx++) {
var file = this.files[idx];
var cur = file.selectedIndex;
if (cur + direction < 0 || cur + direction >= this.files[idx].timeline.length) {
continue;
}
var d = Math.abs(parseInt(file.timeline[cur + direction]) - this.currentTimestamp);
if (timeDiff > d) {
timeDiff = d;
closestTimeline = idx;
}
}
if (closestTimeline >= 0) {
this.files[closestTimeline].selectedIndex += direction;
this.currentTimestamp = parseInt(this.files[closestTimeline].timeline[this.files[closestTimeline].selectedIndex]);
}
},
},
components: {
timeline: Timeline,
datafilter: DataFilter,
}
}
</script>
<style scoped>
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
background: white;
margin: 0;
}
.nav-content {
width: 100%;
}
.toggle-btn {
margin-left: auto;
}
.toolbar, .active-timeline, .options {
display: flex;
flex-direction: row;
flex: 1;
align-items: center;
}
.toolbar-content {
flex-grow: 1;
}
.toolbar-content .seek-time {
margin: 8px 0 -8px 0;
}
.options {
padding: 0 20px 15px 20px
}
.options label {
font-weight: 600;
}
.options .datafilter {
height: 50px;
display: flex;
align-items: center;
}
</style>