Skip to content

Commit f28ba7b

Browse files
authored
Customizable Time Filters (#1550)
* linting fix * prevent multiple tooltips from being appended * add timeFilter for filtering based on time ranges * add time filter button and controls * refactor timeline component, add start/end lines and implement dragging * swap wheel event X location reference * time format util, change initialize function name in Timeline.vue
1 parent 9ab3302 commit f28ba7b

File tree

8 files changed

+762
-181
lines changed

8 files changed

+762
-181
lines changed

client/dive-common/components/ControlsContainer.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export default defineComponent({
152152
dense
153153
style="position:absolute; bottom: 0px; padding: 0px; margin:0px;"
154154
>
155-
<Controls :is-default-image="isDefaultImage">
155+
<Controls :is-default-image="isDefaultImage" :dataset-type="datasetType">
156156
<template slot="timelineControls">
157157
<div style="min-width: 270px">
158158
<v-tooltip
@@ -419,6 +419,7 @@ export default defineComponent({
419419
:max-frame="maxFrame"
420420
:frame="frame"
421421
:display="!collapsed"
422+
:dataset-type="datasetType"
422423
@seek="seek"
423424
>
424425
<template

client/dive-common/components/MultiCamTools.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ export default defineComponent({
142142
</v-row>
143143
<v-divider />
144144
<v-row align="center" justify="space-between" class="mt-4 mb-2">
145-
<h3 class="mb-0">Detection:</h3>
145+
<h3 class="mb-0">
146+
Detection:
147+
</h3>
146148
<div class="d-flex gap-2">
147149
<tooltip-btn
148150
icon="mdi-star"
@@ -168,7 +170,9 @@ export default defineComponent({
168170
<v-divider class="my-2" />
169171

170172
<v-row align="center" justify="space-between" class="mt-2 mb-4">
171-
<h3 class="mb-0">Track:</h3>
173+
<h3 class="mb-0">
174+
Track:
175+
</h3>
172176
<div class="d-flex gap-2">
173177
<tooltip-btn
174178
color="error"

client/src/BaseFilterControls.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export default abstract class BaseFilterControls<T extends Track | Group> {
4949
/* The confidence threshold to test confidecePairs against */
5050
confidenceFilters: Ref<Record<string, number>>;
5151

52+
/* Time filtering values */
53+
timeFilters: Ref<[number, number] | null>;
54+
5255
/* The types informed by meta configuration */
5356
private defaultTypes: Ref<string[]>;
5457

@@ -85,6 +88,8 @@ export default abstract class BaseFilterControls<T extends Track | Group> {
8588

8689
this.confidenceFilters = ref({ default: DefaultConfidence } as Record<string, number>);
8790

91+
this.timeFilters = ref(null);
92+
8893
this.defaultTypes = ref([]);
8994

9095
this.sorted = params.sorted;
@@ -179,6 +184,10 @@ export default abstract class BaseFilterControls<T extends Track | Group> {
179184
}
180185
}
181186

187+
setTimeFilters(val: [number, number] | null) {
188+
this.timeFilters.value = val;
189+
}
190+
182191
updateTypeName({ currentType, newType }: { currentType: string; newType: string }) {
183192
//Go through the entire list and replace the oldType with the new Type
184193
this.sorted.value.forEach((annotation) => {

client/src/TrackFilterControls.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ export default class TrackFilterControls extends BaseFilterControls<Track> {
4343
const resultsArr: AnnotationWithContext<Track>[] = [];
4444
const resultsIds: Set<AnnotationId> = new Set();
4545
params.sorted.value.forEach((annotation) => {
46+
if (this.timeFilters.value !== null) {
47+
const [startTime, endTime] = this.timeFilters.value;
48+
if (annotation.begin > endTime || annotation.end < startTime) {
49+
return;
50+
}
51+
}
4652
let enabledInGroupFilters = true;
4753
const groups = params.lookupGroups(annotation.id);
4854
if (groups.length) {

client/src/components/controls/Controls.vue

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
<script lang="ts">
22
import {
3-
defineComponent, reactive, watch, ref,
3+
defineComponent, reactive, watch, ref, computed,
4+
PropType,
45
} from 'vue';
56
import { usePrompt } from 'dive-common/vue-utilities/prompt-service';
67
import context from 'dive-common/store/context';
78
import { clientSettings } from 'dive-common/store/settings';
9+
import { DatasetType } from 'dive-common/apispec';
10+
import { frameToTimestamp } from 'vue-media-annotator/utils';
811
import { injectAggregateController } from '../annotators/useMediaController';
12+
import { useTime, useTrackFilters } from '../../provides';
913
1014
export default defineComponent({
1115
name: 'Controls',
@@ -14,15 +18,24 @@ export default defineComponent({
1418
type: Boolean as () => boolean,
1519
required: true,
1620
},
21+
datasetType: {
22+
type: String as PropType<DatasetType>,
23+
required: true,
24+
},
25+
1726
},
18-
setup() {
27+
setup(props) {
1928
const data = reactive({
2029
frame: 0,
2130
dragging: false,
2231
});
2332
const mediaController = injectAggregateController().value;
33+
const isVideo = computed(() => props.datasetType === 'video');
34+
const { frameRate } = useTime();
2435
const { visible } = usePrompt();
36+
const trackFilters = useTrackFilters();
2537
const activeLockedCamera = ref(false);
38+
const activeTimeFilter = ref(false);
2639
watch(mediaController.frame, (frame) => {
2740
if (!data.dragging) {
2841
data.frame = frame;
@@ -57,8 +70,42 @@ export default defineComponent({
5770
multBoundsVal.value = !!clientSettings.annotatorPreferences.lockedCamera.multiBounds;
5871
}, { deep: true, immediate: true });
5972
73+
const timeFilterActive = computed(() => trackFilters.timeFilters.value !== null);
74+
const timeFilterMin = computed(() => trackFilters.timeFilters.value?.[0] ?? 0);
75+
const timeFilterMax = computed(() => trackFilters.timeFilters.value?.[1] ?? mediaController.maxFrame.value);
76+
77+
function toggleTimeFilter() {
78+
if (trackFilters.timeFilters.value === null) {
79+
trackFilters.setTimeFilters([0, mediaController.maxFrame.value]);
80+
} else {
81+
trackFilters.setTimeFilters(null);
82+
}
83+
}
84+
85+
function updateTimeFilterMin(value: number) {
86+
const current = trackFilters.timeFilters.value;
87+
if (current) {
88+
trackFilters.setTimeFilters([value, current[1]]);
89+
}
90+
}
91+
92+
function updateTimeFilterMax(value: number) {
93+
const current = trackFilters.timeFilters.value;
94+
if (current) {
95+
trackFilters.setTimeFilters([current[0], value]);
96+
}
97+
}
98+
99+
function formatTimestamp(frame: number) {
100+
if (!isVideo.value || !frameRate.value) {
101+
return null;
102+
}
103+
return frameToTimestamp(frame, frameRate.value);
104+
}
105+
60106
return {
61107
activeLockedCamera,
108+
activeTimeFilter,
62109
data,
63110
mediaController,
64111
dragHandler,
@@ -69,6 +116,15 @@ export default defineComponent({
69116
clientSettings,
70117
transitionVal,
71118
multBoundsVal,
119+
trackFilters,
120+
timeFilterActive,
121+
timeFilterMin,
122+
timeFilterMax,
123+
toggleTimeFilter,
124+
updateTimeFilterMin,
125+
updateTimeFilterMax,
126+
isVideo,
127+
formatTimestamp,
72128
};
73129
},
74130
});
@@ -157,6 +213,97 @@ export default defineComponent({
157213
class="pl-1 py-1 shrink d-flex"
158214
align="right"
159215
>
216+
<v-menu
217+
v-model="activeTimeFilter"
218+
:nudge-left="28"
219+
left
220+
top
221+
:close-on-content-click="false"
222+
open-on-hover
223+
open-delay="750"
224+
close-delay="500"
225+
>
226+
<template #activator="{ on, attrs }">
227+
<v-btn
228+
icon
229+
small
230+
:color="timeFilterActive ? 'primary' : 'default'"
231+
title="Filter tracks by time range"
232+
v-bind="attrs"
233+
v-on="on"
234+
@click="toggleTimeFilter"
235+
>
236+
<v-icon v-bind="attrs" v-on="on">
237+
{{ timeFilterActive ? 'mdi-filter' : 'mdi-filter-outline' }}
238+
</v-icon>
239+
</v-btn>
240+
</template>
241+
<v-card
242+
outlined
243+
class="pa-2 pr-4"
244+
color="blue-grey darken-3"
245+
style="overflow-y: none"
246+
>
247+
<v-card-title>
248+
Time Filter Settings
249+
</v-card-title>
250+
<v-card-text>
251+
<v-row class="align-center" dense>
252+
<v-col>
253+
<div class="text-caption mb-2">
254+
Filter tracks to only show those that intersect with this time range.
255+
</div>
256+
</v-col>
257+
</v-row>
258+
<div v-if="timeFilterActive">
259+
<v-row class="align-center" dense>
260+
<v-col>
261+
Min Frame:
262+
</v-col>
263+
<v-col v-if="isVideo">
264+
{{ formatTimestamp(timeFilterMin) }}
265+
</v-col>
266+
<v-col>
267+
<v-slider
268+
:value="timeFilterMin"
269+
:min="0"
270+
:max="mediaController.maxFrame.value"
271+
step="1"
272+
dense
273+
hide-details
274+
thumb-label="always"
275+
@change="updateTimeFilterMin"
276+
/>
277+
</v-col>
278+
</v-row>
279+
<v-row class="align-center" dense>
280+
<v-col>
281+
Max Frame:
282+
</v-col>
283+
<v-col v-if="isVideo">
284+
{{ formatTimestamp(timeFilterMax) }}
285+
</v-col>
286+
287+
<v-col>
288+
<v-slider
289+
:value="timeFilterMax"
290+
:min="0"
291+
:max="mediaController.maxFrame.value"
292+
step="1"
293+
dense
294+
hide-details
295+
thumb-label="always"
296+
@change="updateTimeFilterMax"
297+
/>
298+
</v-col>
299+
</v-row>
300+
</div>
301+
<div v-else>
302+
<p>Click the filter icon to enable time filtering</p>
303+
</div>
304+
</v-card-text>
305+
</v-card>
306+
</v-menu>
160307
<v-menu
161308
v-model="activeLockedCamera"
162309
:nudge-left="28"

client/src/components/controls/LineChart.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export default Vue.extend({
8080
.select('svg')
8181
.remove();
8282
let tooltipTimeoutHandle = null;
83+
d3.select('.tooltip').remove();
8384
const tooltip = d3
8485
.select(this.$el)
8586
.append('div')

0 commit comments

Comments
 (0)