feat(frontend): filtering
This commit is contained in:
+28
-14
@@ -2,7 +2,12 @@
|
||||
<div class="mt-3">
|
||||
<portal to="filter-actions">
|
||||
<v-list-item-action class="pa-0 ma-0">
|
||||
<v-btn v-tooltip="''" small icon @click.stop="colorBy = !colorBy">
|
||||
<v-btn
|
||||
v-tooltip="'Set colors automatically based on each property'"
|
||||
small
|
||||
icon
|
||||
@click.stop="colorBy = !colorBy"
|
||||
>
|
||||
<v-icon small :class="`${colorBy ? 'primary--text' : ''}`">mdi-palette</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
@@ -22,21 +27,21 @@
|
||||
</v-col>
|
||||
<v-col
|
||||
v-tooltip="type.fullName"
|
||||
cols="8"
|
||||
cols="7"
|
||||
:class="`caption text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
|
||||
style="line-height: 24px"
|
||||
>
|
||||
{{ type.name }}
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="3"
|
||||
cols="4"
|
||||
:class="`caption text-truncate text-right px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
|
||||
style="line-height: 24px"
|
||||
>
|
||||
<div
|
||||
v-if="colorBy"
|
||||
class="d-inline-block rounded"
|
||||
:style="`width: 10px; height: 10px`"
|
||||
class="d-inline-block rounded mr-3 mt-1 elevation-3"
|
||||
:style="`width: 8px; height: 8px; background:${legend[type.fullName]};`"
|
||||
></div>
|
||||
<v-btn
|
||||
v-tooltip="'Toggle visibility'"
|
||||
@@ -74,8 +79,7 @@ export default {
|
||||
filter: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
active: { type: Boolean, default: false }
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -84,7 +88,7 @@ export default {
|
||||
typeMap: [],
|
||||
colorBy: false,
|
||||
appliedFilter: {},
|
||||
legend: null
|
||||
legend: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -96,7 +100,6 @@ export default {
|
||||
? { type: 'category', property: this.filter.targetKey }
|
||||
: null
|
||||
let res = await window.__viewer.applyFilter(this.appliedFilter)
|
||||
console.log(res)
|
||||
this.mashColorLegend(res.colorLegend)
|
||||
}
|
||||
},
|
||||
@@ -107,9 +110,18 @@ export default {
|
||||
window.__viewer.applyFilter(null)
|
||||
},
|
||||
methods: {
|
||||
mashColorLegend(thanksCristi) {
|
||||
if (!thanksCristi) return
|
||||
// TODO
|
||||
mashColorLegend(colorLegend) {
|
||||
// just adds to our colors
|
||||
if (!colorLegend) return
|
||||
let keys = Object.keys(colorLegend)
|
||||
for (const key of keys) {
|
||||
if (!this.legend[key]) this.$set(this.legend, key, colorLegend[key])
|
||||
//this.legend[key] = colorLegend[key]
|
||||
// const idx = this.legend.indexOf((o) => o.name === key)
|
||||
// if (idx === -1) {
|
||||
// this.legend.push({ name: key, color: colorLegend[key] })
|
||||
// }
|
||||
}
|
||||
},
|
||||
async toggleFilter(type) {
|
||||
let indx = this.filtered.indexOf(type)
|
||||
@@ -117,9 +129,10 @@ export default {
|
||||
else this.filtered.splice(indx, 1)
|
||||
this.hidden.splice(0, this.hidden.length)
|
||||
if (this.filtered.length === 0) {
|
||||
window.__viewer.applyFilter(
|
||||
let res = window.__viewer.applyFilter(
|
||||
this.colorBy ? { colorBy: { type: 'category', property: this.filter.targetKey } } : null
|
||||
)
|
||||
this.mashColorLegend(res.colorLegend)
|
||||
this.appliedFilter = {}
|
||||
} else {
|
||||
let filterObj = {
|
||||
@@ -139,9 +152,10 @@ export default {
|
||||
else this.hidden.splice(indx, 1)
|
||||
this.filtered.splice(0, this.filtered.length)
|
||||
if (this.hidden.length === 0) {
|
||||
window.__viewer.applyFilter(
|
||||
let res = await window.__viewer.applyFilter(
|
||||
this.colorBy ? { colorBy: { type: 'category', property: this.filter.targetKey } } : null
|
||||
)
|
||||
this.mashColorLegend(res.colorLegend)
|
||||
this.appliedFilter = {}
|
||||
} else {
|
||||
let filterObj = {
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="mt-3">
|
||||
<portal to="filter-actions">
|
||||
<v-list-item-action class="pa-0 ma-0">
|
||||
<v-btn
|
||||
v-tooltip="'Set colors automatically based on each property'"
|
||||
small
|
||||
icon
|
||||
@click.stop="colorBy = !colorBy"
|
||||
>
|
||||
<v-icon small :class="`${colorBy ? 'primary--text' : ''}`">mdi-palette</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</portal>
|
||||
<v-row no-gutters class="my-1 property-row rounded-lg">
|
||||
<v-col
|
||||
cols="1"
|
||||
:class="`text-center text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
|
||||
style="line-height: 24px; font-size: 9px"
|
||||
>
|
||||
<v-icon small style="font-size: 12px" :class="`${$vuetify.theme.dark ? 'grey--text' : ''}`">
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="11"
|
||||
:class="`caption text-truncatexxx px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
|
||||
style="line-height: 24px"
|
||||
>
|
||||
{{ filter.data.objectCount }} elements; min: {{ Math.round(filter.data.minValue, 2) }}; max:
|
||||
{{ Math.round(filter.data.maxValue, 2) }}
|
||||
<v-btn
|
||||
v-show="range[0] !== filter.data.minValue || range[1] !== filter.data.maxValue"
|
||||
v-tooltip="'Reset'"
|
||||
x-small
|
||||
icon
|
||||
class="mr-1 float-right"
|
||||
@click="
|
||||
$set(range, 0, filter.data.minValue)
|
||||
$set(range, 1, filter.data.maxValue)
|
||||
setFilter()
|
||||
"
|
||||
>
|
||||
<v-icon class="grey--text mt-1" style="font-size: 12px">mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="filter.data.maxValue === filter.data.minValue"
|
||||
cols="12"
|
||||
:class="`caption text-truncatexxx px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
|
||||
style="line-height: 24px"
|
||||
>
|
||||
Invalid values (min value equals to max value).
|
||||
</v-col>
|
||||
<v-col v-else cols="12" class="mt-5 py-5 px-5">
|
||||
<v-range-slider
|
||||
v-model="range"
|
||||
dense
|
||||
hide-details
|
||||
thumb-label="always"
|
||||
color="primary"
|
||||
:step="0.01"
|
||||
:max="filter.data.maxValue"
|
||||
:min="filter.data.minValue"
|
||||
:class="`${colorBy ? 'super-slider' : ''}`"
|
||||
@change="setFilter()"
|
||||
>
|
||||
<template #thumb-label="{ value }">{{ value | prettynum }}</template>
|
||||
</v-range-slider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
filter: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
active: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
range: [0, 1],
|
||||
appliedFilter: {},
|
||||
legend: {},
|
||||
colorBy: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filter(newVal) {
|
||||
this.$set(this.range, 0, newVal.data.minValue)
|
||||
this.$set(this.range, 1, newVal.data.maxValue)
|
||||
},
|
||||
colorBy() {
|
||||
this.setFilter()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$set(this.range, 0, this.filter.data.minValue)
|
||||
this.$set(this.range, 1, this.filter.data.maxValue)
|
||||
this.setFilter()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.__viewer.applyFilter(null)
|
||||
},
|
||||
methods: {
|
||||
async setFilter() {
|
||||
console.log(this.range)
|
||||
let filterObj = {
|
||||
filterBy: {},
|
||||
colorBy: this.colorBy
|
||||
? {
|
||||
type: 'gradient',
|
||||
property: this.filter.targetKey,
|
||||
minValue: this.range[0],
|
||||
maxValue: this.range[1],
|
||||
gradientColors: ['#3F5EFB', '#FC466B']
|
||||
}
|
||||
: null,
|
||||
ghostOthers: true
|
||||
}
|
||||
filterObj.filterBy[this.filter.targetKey] = { gte: this.range[0], lte: this.range[1] }
|
||||
await window.__viewer.applyFilter(filterObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.property-row {
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(120, 120, 120, 0.05);
|
||||
}
|
||||
.property-row:hover {
|
||||
background: rgba(120, 120, 120, 0.09);
|
||||
}
|
||||
.super-slider .v-slider__track-fill {
|
||||
background: linear-gradient(to left, #fc466b, #3f5efb) !important;
|
||||
background: -webkit-linear-gradient(to left, #fc466b, #3f5efb); /* Chrome 10-25, Safari 5.1-6 */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<v-row no-gutters class="my-1 property-row rounded-lg">
|
||||
<v-col cols="1" class="text-center" style="line-height: 30px">
|
||||
<v-icon small style="font-size: 12px" :class="`${$vuetify.theme.dark ? 'grey--text' : ''}`">
|
||||
{{ filter.data.type === 'number' ? 'mdi-numeric' : 'mdi-format-text' }}
|
||||
</v-icon>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
:class="`caption text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
|
||||
style="line-height: 30px"
|
||||
>
|
||||
<span v-tooltip="filter.targetKey">{{ filter.name }}</span>
|
||||
<span v-tooltip="'Matching objects'" style="font-size: 10px">
|
||||
({{ filter.data.objectCount }})
|
||||
</span>
|
||||
</v-col>
|
||||
<v-col class="text-right" style="line-height: 30px">
|
||||
<v-btn x-small @click="$emit('active-toggle', filter)">Set</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
filter: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.property-row {
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(120, 120, 120, 0.05);
|
||||
}
|
||||
.property-row:hover {
|
||||
background: rgba(120, 120, 120, 0.09);
|
||||
}
|
||||
</style>
|
||||
@@ -1,119 +0,0 @@
|
||||
<template>
|
||||
<v-row no-gutters class="my-1 property-row rounded-lg">
|
||||
<v-col cols="1" class="text-center" style="line-height: 30px">
|
||||
<v-icon small style="font-size: 12px" :class="`${$vuetify.theme.dark ? 'grey--text' : ''}`">
|
||||
{{ filter.data.type === 'number' ? 'mdi-numeric' : 'mdi-format-text' }}
|
||||
</v-icon>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-tooltip="filter.targetKey"
|
||||
cols="8"
|
||||
:class="`caption text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
|
||||
style="line-height: 30px"
|
||||
>
|
||||
{{ filter.name }}
|
||||
</v-col>
|
||||
<v-col class="text-right" style="line-height: 30px">
|
||||
<v-btn x-small @click="$emit('active-toggle', filter)">{{ active ? 'Remove' : 'Set' }}</v-btn>
|
||||
</v-col>
|
||||
<v-scroll-y-transition>
|
||||
<v-col v-if="active && filter.data.type === 'string'" cols="12">
|
||||
<!-- <div v-if="filter.data.type === 'string'"> -->
|
||||
<v-row
|
||||
v-for="type in typeMap"
|
||||
:key="type.fullName"
|
||||
no-gutters
|
||||
class="my-1 property-row rounded-lg"
|
||||
>
|
||||
<v-col
|
||||
cols="1"
|
||||
:class="`caption text-center text-truncate px-1 ${
|
||||
$vuetify.theme.dark ? 'grey--text' : ''
|
||||
}`"
|
||||
style="line-height: 24px; font-size: 10px"
|
||||
>
|
||||
{{ type.count }}
|
||||
</v-col>
|
||||
<v-col
|
||||
v-tooltip="type.fullName"
|
||||
cols="8"
|
||||
:class="`caption text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
|
||||
style="line-height: 24px"
|
||||
>
|
||||
{{ type.name }}
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="3"
|
||||
:class="`caption text-truncate text-right px-1 ${
|
||||
$vuetify.theme.dark ? 'grey--text' : ''
|
||||
}`"
|
||||
style="line-height: 24px"
|
||||
>
|
||||
<v-btn
|
||||
v-tooltip="'Toggle visibility'"
|
||||
x-small
|
||||
icon
|
||||
class="mr-1"
|
||||
@click="toggleVisibility()"
|
||||
>
|
||||
<v-icon class="grey--text" style="font-size: 11px">
|
||||
{{ visible ? 'mdi-eye' : 'mdi-eye-off' }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-tooltip="'Isolate objects'" x-small icon class="mr-1" @click="toggleFilter()">
|
||||
<v-icon
|
||||
:class="`${filtered ? 'primary--text' : 'grey--text'}`"
|
||||
style="font-size: 11px"
|
||||
>
|
||||
{{ !filtered ? 'mdi-filter' : 'mdi-filter' }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!-- </div> -->
|
||||
</v-col>
|
||||
</v-scroll-y-transition>
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
filter: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
active: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: true,
|
||||
filtered: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
typeMap() {
|
||||
if (this.filter.data.type !== 'string') return []
|
||||
let typeMap = []
|
||||
for (let key of Object.keys(this.filter.data.uniqueValues)) {
|
||||
let shortName = key.split('.').reverse()[0]
|
||||
typeMap.push({
|
||||
name: shortName,
|
||||
fullName: key,
|
||||
count: this.filter.data.uniqueValues[key]
|
||||
})
|
||||
}
|
||||
return typeMap
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.property-row {
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(120, 120, 120, 0.05);
|
||||
}
|
||||
.property-row:hover {
|
||||
background: rgba(120, 120, 120, 0.09);
|
||||
}
|
||||
</style>
|
||||
@@ -39,8 +39,11 @@
|
||||
</v-list-item>
|
||||
<v-scroll-y-transition>
|
||||
<div v-show="expand">
|
||||
<div v-if="activeFilter" class="px-0">
|
||||
<filter-active :filter="activeFilter" />
|
||||
<div v-if="activeFilter && activeFilter.data.type === 'string'">
|
||||
<filter-category-active :filter="activeFilter" />
|
||||
</div>
|
||||
<div v-if="activeFilter && activeFilter.data.type === 'number'">
|
||||
<filter-numeric-active :filter="activeFilter" />
|
||||
</div>
|
||||
<div v-show="activeFilter === null">
|
||||
<v-subheader>TODO: reccommended filters</v-subheader>
|
||||
@@ -56,7 +59,7 @@
|
||||
style="position: sticky; top: 110px; z-index: 6"
|
||||
/>
|
||||
<div v-for="filter in matchingFilters" :key="filter.targetKey">
|
||||
<filter-single :filter="filter" @active-toggle="(e) => (activeFilter = e)" />
|
||||
<filter-row-select :filter="filter" @active-toggle="(e) => (activeFilter = e)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,8 +70,9 @@
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
FilterSingle: () => import('@/cleanup/components/viewer/FilterSingle'),
|
||||
FilterActive: () => import('@/cleanup/components/viewer/FilterActive')
|
||||
FilterRowSelect: () => import('@/cleanup/components/viewer/FilterRowSelect'),
|
||||
FilterCategoryActive: () => import('@/cleanup/components/viewer/FilterCategoryActive'),
|
||||
FilterNumericActive: () => import('@/cleanup/components/viewer/FilterNumericActive')
|
||||
},
|
||||
props: {
|
||||
props: {
|
||||
@@ -175,7 +179,6 @@ export default {
|
||||
filter.data = this.props[key]
|
||||
filters.push(filter)
|
||||
}
|
||||
console.log(filters)
|
||||
this.allFilters = filters
|
||||
}
|
||||
}
|
||||
@@ -190,7 +193,4 @@ export default {
|
||||
background: rgba(235, 235, 235, 1);
|
||||
z-index: 5;
|
||||
}
|
||||
.ps {
|
||||
height: 50vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -232,22 +232,26 @@ export default {
|
||||
this.calls++
|
||||
if (this.calls !== 2) return
|
||||
console.log('load start')
|
||||
await window.__viewer.loadObject(
|
||||
`${window.location.origin}/streams/${this.stream.id}/objects/${this.stream.commit.referencedObject}`
|
||||
)
|
||||
window.__viewer.zoomExtents(undefined, false)
|
||||
this.loadedModel = true
|
||||
console.log('load end')
|
||||
try {
|
||||
this.objectProperties = await window.__viewer.getObjectsProperties()
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: 'Failed to get object properties from viewer.'
|
||||
})
|
||||
}
|
||||
this.views.splice(0, this.views.length)
|
||||
console.log()
|
||||
this.views.push(...window.__viewer.sceneManager.views)
|
||||
// TODO: issue when freshly logged in, this throws an error
|
||||
// that window.__viewer is null.
|
||||
this.$nextTick(async () => {
|
||||
await window.__viewer.loadObject(
|
||||
`${window.location.origin}/streams/${this.stream.id}/objects/${this.stream.commit.referencedObject}`
|
||||
)
|
||||
window.__viewer.zoomExtents(undefined, false)
|
||||
this.loadedModel = true
|
||||
console.log('load end')
|
||||
try {
|
||||
this.objectProperties = await window.__viewer.getObjectsProperties()
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: 'Failed to get object properties from viewer.'
|
||||
})
|
||||
}
|
||||
this.views.splice(0, this.views.length)
|
||||
console.log()
|
||||
this.views.push(...window.__viewer.sceneManager.views)
|
||||
})
|
||||
},
|
||||
captureProgress(args) {
|
||||
this.loadProgress = args.progress * 100
|
||||
|
||||
Reference in New Issue
Block a user