Files
dougal-software/lib/www/client/source/src/views/SequenceList.vue

643 lines
20 KiB
Vue
Raw Normal View History

2020-08-08 23:59:13 +02:00
<template>
2020-08-25 13:02:20 +02:00
<v-container fluid>
<v-card>
<v-card-title>
<v-toolbar flat>
<v-toolbar-title>Sequences</v-toolbar-title>
<v-spacer></v-spacer>
<v-text-field
v-model="filter"
append-icon="mdi-magnify"
label="Filter"
single-line
clearable
hint="Filter by sequence, line, date or remarks"
></v-text-field>
</v-toolbar>
</v-card-title>
<v-card-text>
<v-menu
v-model="contextMenuShow"
:position-x="contextMenuX"
:position-y="contextMenuY"
absolute
offset-y
>
<v-list dense v-if="contextMenuItem">
<v-list-item @click="addToPlan(false)">
<v-list-item-title>Reshoot</v-list-item-title>
</v-list-item>
<v-list-item @click="addToPlan(true)">
<v-list-item-title>Reshoot with overlap</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
2020-08-25 13:02:20 +02:00
<v-data-table
:headers="headers"
:items="items"
:items-per-page.sync="itemsPerPage"
item-key="sequence"
:server-items-length="num_rows"
:search="filter"
:custom-filter="customFilter"
:loading="loading"
:fixed-header="true"
:footer-props='{itemsPerPageOptions: [ 10, 25, 50, 100, 500, -1 ]}'
show-expand
:item-class="(item) => activeItem == item ? 'blue accent-1 elevation-3' : ''"
@click:row="setActiveItem"
@contextmenu:row="contextMenu"
2020-08-25 13:02:20 +02:00
>
2020-08-25 18:04:42 +02:00
<template v-slot:expanded-item="{ headers, item }">
<td :colspan="headers.length" class="pa-0">
<v-container fluid class="pa-0">
<v-row no-gutters class="d-flex flex-column flex-sm-row">
<v-col cols="6" class="d-flex flex-column">
<v-card outlined class="flex-grow-1">
<v-card-title>
Acquisition remarks
<v-btn v-if="edit && edit.sequence == item.sequence && edit.key == 'remarks'"
class="ml-3"
icon
small
title="Save edits"
:disabled="loading"
@click="edit = null"
>
<v-icon small>mdi-content-save-edit-outline</v-icon>
</v-btn>
<v-btn v-else-if="edit === null"
class="ml-3"
icon
small
title="Edit"
:disabled="loading"
@click="editItem(item, 'remarks')"
>
<v-icon small>mdi-square-edit-outline</v-icon>
</v-btn>
</v-card-title>
<v-card-subtitle>
</v-card-subtitle>
<v-card-text v-if="edit && edit.sequence == item.sequence && edit.key == 'remarks'">
<v-textarea
autofocus
placeholder="Enter your text here"
:disabled="loading"
v-model="edit.value"
>
</v-textarea>
</v-card-text>
<v-card-text v-else>
{{ item.remarks }}
</v-card-text>
</v-card>
<v-card outlined class="flex-grow-1" v-if="item.remarks_final !== null">
<v-card-title>
Processing remarks
<v-btn v-if="edit && edit.sequence == item.sequence && edit.key == 'remarks_final'"
class="ml-3"
icon
small
title="Save edits"
:disabled="loading"
@click="edit = null"
>
<v-icon small>mdi-content-save-edit-outline</v-icon>
</v-btn>
<v-btn v-else-if="edit === null"
class="ml-3"
icon
small
title="Edit"
:disabled="loading"
@click="editItem(item, 'remarks_final')"
>
<v-icon small>mdi-square-edit-outline</v-icon>
</v-btn>
</v-card-title>
<v-card-subtitle>
</v-card-subtitle>
<v-card-text v-if="edit && edit.sequence == item.sequence && edit.key == 'remarks_final'">
<v-textarea
autofocus
placeholder="Enter your text here"
:disabled="loading"
v-model="edit.value"
>
</v-textarea>
</v-card-text>
<v-card-text>
{{ item.remarks_final }}
</v-card-text>
</v-card>
</v-col>
<v-col cols="6" class="d-flex">
<v-card outlined class="flex-grow-1">
<v-card-title>
Source files
</v-card-title>
2020-08-25 18:04:42 +02:00
<v-card-subtitle>
</v-card-subtitle>
<v-card-text>
<v-list>
<v-list-group value="true" v-if="item.raw_files">
2020-08-25 18:04:42 +02:00
<template v-slot:activator>
<v-list-item-title>
Raw files
<span class="grey--text text--lighten-1">
{{item.raw_files.length}}
</span>
</v-list-item-title>
</template>
<v-list-item v-for="(path, index) in item.raw_files"
key="index"
link
title="View the shot log"
>
{{ basename(path) }}
</v-list-item>
</v-list-group>
<v-list-group value="true" v-if="item.final_files">
2020-08-25 18:04:42 +02:00
<template v-slot:activator>
<v-list-item-title>
Final files
<span class="grey--text text--lighten-1">
{{item.final_files.length}}
</span>
</v-list-item-title>
</template>
<v-list-item v-for="(path, index) in item.final_files"
key="index"
link
title="View the shot log"
>
{{ basename(path) }}
</v-list-item>
</v-list-group>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</td>
</template>
<template v-slot:item.sequence="{value}">
<a
:href="`/projects/${$route.params.project}/log/sequence/${value}`"
title="View the event log for this sequence">{{value}}</a>
</template>
<template v-slot:item.status="{value, item}">
<span :class="{'success--text': value=='final', 'warning--text': value=='raw', 'error--text': value=='ntbp'}">
{{ value == "final" ? "Processed" : value == "raw" ? item.raw_files ? "Acquired" : "In acquisition" : value == "ntbp" ? "NTBP" : `Unknown (${status})` }}
2020-08-25 13:02:20 +02:00
</span>
</template>
<template v-slot:item.duration="{item: {duration: value}}">
{{
value
?
"" +
(value.days
? value.days + "d "
: "") +
String(value.hours || 0).padStart(2, "0") +
":" + String(value.minutes || 0).padStart(2, "0") +
":" + String(value.seconds || 0).padStart(2, "0")
: "N/A"
}}
</template>
<template v-slot:item.duration_final="{item: {duration_final: value}}">
{{
value
?
"" +
(value.days
? value.days + "d "
: "") +
String(value.hours || 0).padStart(2, "0") +
":" + String(value.minutes || 0).padStart(2, "0") +
":" + String(value.seconds || 0).padStart(2, "0")
: "N/A"
}}
</template>
<template v-slot:item.ts0="{value}">
<span v-if="value">
{{ value.replace(/(.{10})T(.{8}).{4}Z$/, "$1 $2") }}
</span>
</template>
<template v-slot:item.ts1="{value}">
<span v-if="value">
{{ value.replace(/(.{10})T(.{8}).{4}Z$/, "$1 $2") }}
</span>
</template>
<template v-slot:item.ts0_final="{value}">
<span v-if="value">
{{ value.replace(/(.{10})T(.{8}).{4}Z$/, "$1 $2") }}
</span>
</template>
<template v-slot:item.ts1_final="{value}">
<span v-if="value">
{{ value.replace(/(.{10})T(.{8}).{4}Z$/, "$1 $2") }}
</span>
</template>
<template v-slot:item.missing_shots="{value}">
<span :class="value && 'warning--text'">{{ value }}</span>
</template>
<template v-slot:item.length="props">
<span>{{ Math.round(props.value) }} m</span>
</template>
<template v-slot:item.azimuth="{value}">
<span>{{ value.toFixed? value.toFixed(1) : value }} °</span>
2020-08-25 13:02:20 +02:00
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
2020-08-08 23:59:13 +02:00
</template>
2020-09-12 19:16:18 +02:00
<style scoped>
2020-08-25 13:02:20 +02:00
td span {
white-space: nowrap;
}
.status-raw {
color: orange;
}
.status-final {
color: green;
}
.status-ntbp {
color: red;
}
tr :nth-child(5), tr :nth-child(8), tr :nth-child(11), tr :nth-child(14) {
opacity: 0.7;
}
2020-08-25 13:02:20 +02:00
</style>
2020-08-08 23:59:13 +02:00
<script>
import { mapActions, mapGetters } from 'vuex';
2020-08-25 18:04:42 +02:00
import { basename } from 'path';
2020-08-08 23:59:13 +02:00
export default {
2020-08-25 13:02:20 +02:00
name: "SequenceList",
data () {
return {
headers: [
2020-08-25 18:04:42 +02:00
{
value: 'data-table-expand'
},
2020-08-25 13:02:20 +02:00
{
value: "sequence",
text: "Sequence"
},
{
value: "status",
text: "Status"
},
{
value: "line",
text: "Line"
},
2020-09-08 17:46:25 +02:00
{
value: "fsp",
text: "FSP",
align: "end"
},
2020-08-25 13:02:20 +02:00
{
value: "fsp_final",
text: "FPSP",
align: "end"
},
{
value: "lsp_final",
text: "LPSP",
align: "end"
},
{
value: "lsp",
text: "LSP",
align: "end"
},
{
value: "duration_final",
text: "Prime duration",
align: "end"
},
{
value: "duration",
text: "Total duration",
align: "end"
},
2020-09-08 17:46:25 +02:00
{
value: "ts0",
text: "Start time",
align: "end"
},
2020-08-25 13:02:20 +02:00
{
value: "ts0_final",
text: "FPSP time",
align: "end"
},
{
value: "ts1_final",
text: "LPSP time",
align: "end"
},
{
value: "ts1",
text: "End time",
align: "end"
},
{
value: "num_points",
text: "Shots acquired",
align: "end"
},
{
value: "missing_shots",
text: "Shots missed",
align: "end"
},
{
value: "length",
text: "Length",
align: "end"
},
{
value: "azimuth",
text: "Azimuth",
align: "end"
}
],
2020-08-25 18:04:42 +02:00
expanded: [],
2020-08-25 13:02:20 +02:00
items: [],
filter: "",
options: {},
num_rows: null,
activeItem: null,
edit: null, // {sequence, key, value}
queuedReload: false,
itemsPerPage: 25,
// Planner related stuff
preplots: null,
plannerConfig: null,
// Context menu stuff
contextMenuShow: false,
contextMenuX: 0,
contextMenuY: 0,
contextMenuItem: null
2020-08-25 13:02:20 +02:00
}
},
computed: {
...mapGetters(['user', 'loading', 'serverEvent'])
},
2020-08-25 13:02:20 +02:00
watch: {
options: {
handler () {
this.getSequences();
},
deep: true
},
async edit (newVal, oldVal) {
if (newVal === null && oldVal !== null) {
const item = this.items.find(i => i.sequence == oldVal.sequence);
if (item && item[oldVal.key] != oldVal.value) {
if (await this.saveItem(oldVal)) {
item[oldVal.key] = oldVal.value;
} else {
this.edit = oldVal;
}
}
}
},
async serverEvent (event) {
const subscriptions = ["raw_lines", "final_lines", "final_shots"];
if (subscriptions.includes(event.channel) && event.payload.pid == this.$route.params.project) {
if (!this.loading && !this.queuedReload) {
// Do not force a non-cached response if refreshing as a result
// of an event notification. We will assume that the server has
// already had time to update the cache by the time our request
// gets back to it.
this.getSequences();
} else {
this.queuedReload = true;
}
}
},
queuedReload (newVal, oldVal) {
if (newVal && !oldVal && !this.loading) {
this.getSequences();
}
},
loading (newVal, oldVal) {
if (!newVal && oldVal && this.queuedReload) {
this.getSequences();
}
},
itemsPerPage (newVal, oldVal) {
localStorage.setItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`, newVal);
},
user (newVal, oldVal) {
this.itemsPerPage = Number(localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`)) || 25;
2020-08-25 13:02:20 +02:00
}
2020-08-25 13:02:20 +02:00
},
methods: {
contextMenu (e, {item}) {
e.preventDefault();
this.contextMenuShow = false;
this.contextMenuX = e.clientX;
this.contextMenuY = e.clientY;
this.contextMenuItem = item;
this.$nextTick( () => this.contextMenuShow = true );
},
async getReshootEndpoints (item, overlap) {
const urlPreplot = `/project/${this.$route.params.project}/line`;
const urlPlannerConfig = `/project/${this.$route.params.project}/configuration/planner`;
if (!this.preplots) {
this.preplots = await this.api([urlPreplot]);
}
if (!this.plannerConfig) {
this.plannerConfig = await this.api([urlPlannerConfig]) || {
overlapBefore: 0,
overlapAfter: 0
};
}
const preplot = this.preplots.find(l => l.line == item.line);
const incr = item.fsp <= item.lsp;
const lim0 = incr
? Math.max : Math.min;
const lim1 = incr
? Math.min : Math.max;
const dir = incr ? 1 : -1;
const sp0 = overlap
? lim0((item.fsp_final || item.fsp) - this.plannerConfig.overlapBefore * dir, preplot.fsp)
: lim0(item.fsp_final || item.fsp, preplot.fsp);
const sp1 = overlap
? lim1((item.lsp_final || item.lsp) + this.plannerConfig.overlapAfter * dir, preplot.lsp)
: lim1(item.lsp_final || item.lsp, preplot.lsp);
return {sp0, sp1}
},
async addToPlan (overlap=false) {
const { sp0, sp1 } = await this.getReshootEndpoints(this.contextMenuItem, overlap);
const payload = {
line: this.contextMenuItem.line,
fsp: sp0,
lsp: sp1,
remarks: `Reshoot of sequence ${this.contextMenuItem.sequence}.`
}
console.log("Plan", payload);
const url = `/project/${this.$route.params.project}/plan`;
const init = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: payload
}
await this.api([url, init]);
},
editItem (item, key) {
this.edit = {
sequence: item.sequence,
key,
value: item[key]
}
},
async saveItem (edit) {
if (!edit) return;
try {
const url = `/project/${this.$route.params.project}/sequence/${edit.sequence}`;
const init = {
method: "PATCH",
body: {
[edit.key]: edit.value
}
};
let res;
await this.api([url, init, (e, r) => res = r]);
return res && res.ok;
} catch (err) {
return false;
}
},
2020-08-25 13:02:20 +02:00
setActiveItem (item) {
this.activeItem = this.activeItem == item
? null
: item;
},
2020-08-25 13:02:20 +02:00
async getNumLines () {
const projectInfo = await this.api([`/project/${this.$route.params.project}`]);
this.num_rows = projectInfo.sequences;
},
async getSequences () {
const query = new URLSearchParams(this.options);
query.set("filter", this.filter);
2020-08-25 18:04:42 +02:00
query.set("files", true);
2020-08-25 13:02:20 +02:00
if (this.options.itemsPerPage < 0) {
query.delete("itemsPerPage");
}
const url = `/project/${this.$route.params.project}/sequence?${query.toString()}`;
this.queuedReload = false;
2020-08-25 13:02:20 +02:00
this.items = await this.api([url]) || [];
},
2020-08-25 18:04:42 +02:00
basename (path, ext) {
return basename(path, ext);
},
2020-09-02 11:09:04 +02:00
customFilter (value, search, item) {
if (!search) return true;
const number = Number(search);
if (!isNaN(number)) {
if (item.sequence == number) return true;
if (search.length > 3) {
const searchShots = [ "line", "fsp", "lsp", "fsp_final", "lsp_final" ].some( k =>
2020-09-02 11:09:04 +02:00
item[k] == number
);
if (searchShots) return true;
}
}
if (search.length > 2) {
const searchDates = [ "ts0", "ts1" ].some(k => {
const i = item[k].indexOf(search);
return i >= 0 && i < 10;
});
if (searchDates) return true;
}
if ((item.remarks||"").indexOf(search) != -1) return true;
if ((item.remarks_final||"").indexOf(search) != -1) return true;
if (item.status.indexOf(search.toLowerCase()) == 0) return true;
return false;
},
2020-08-25 13:02:20 +02:00
...mapActions(["api"])
},
mounted () {
this.getSequences();
this.getNumLines();
}
2020-08-08 23:59:13 +02:00
}
</script>