Implement the SequenceSummary component as a shotlog

This commit is contained in:
D. Berge
2023-10-31 10:33:56 +01:00
parent 1576b121e6
commit 1bb5e2a41d

View File

@@ -1,13 +1,570 @@
<template>
<div>
SequenceSummary
</div>
<v-container fluid>
<v-card>
<v-card-title>
<v-progress-linear indeterminate v-if="loading"></v-progress-linear>
<v-toolbar flat>
<v-toolbar-title>
Sequence {{sequenceNumber}}
<small :class="statusColour" v-if="sequence">({{sequence.status}})</small>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-text-field
v-model="filter"
append-icon="mdi-magnify"
label="Filter shots"
single-line
clearable
hint="Filter by point, line, time, etc."
></v-text-field>
</v-toolbar>
</v-card-title>
<v-card-text>
<v-container fluid>
<v-row>
<v-col cols="6" class="pa-0">
<v-card outlined tile height="100%" class="flex-grow-1">
<v-card-subtitle>
Acquisition remarks
</v-card-subtitle>
<v-card-text v-html="$options.filters.markdown(remarks)">
</v-card-text>
</v-card>
</v-col>
<v-col cols="6" class="pa-0">
<v-card outlined tile height="100%" class="flex-grow-1">
<v-card-subtitle>
Processing remarks
</v-card-subtitle>
<v-card-text v-html="$options.filters.markdown(remarks_final)">
</v-card-text>
</v-card>
</v-col>
</v-row>
<template v-if="sequence">
<v-row>
<v-col v-for="(col, idx) in infoColumns" :key="idx" cols="3">
<b :title="col.title">{{col.text}}:</b> {{ col.value }}
</v-col>
</v-row>
</template>
</v-container>
<v-card outlined tile class="flex-grow-1">
<v-card-subtitle>
Shot log
</v-card-subtitle>
<v-card-text>
<v-data-table
:headers="shotlogHeaders"
:items="shots"
item-key="point"
:search="filter"
:custom-sort="customSorter"
fixed-header
show-expand
@click:row="onRowClicked"
>
<template v-slot:item.position="{item}">
<a v-if="position(item).latitude"
:href="`/projects/${$route.params.project}/map#15/${position(item).longitude}/${position(item).latitude}`"
title="View on map"
:target="`/projects/${$route.params.project}/map`"
@click.stop
>
<small>{{ format_position(position(item)) }}</small>
</a>
</template>
<template v-slot:item.ε_inline="{item}">
{{ ε(item).inline }}
</template>
<template v-slot:item.ε_crossline="{item}">
{{ ε(item).crossline }}
</template>
<template v-slot:item.co_inline="{item}">
{{ co(item).inline }}
</template>
<template v-slot:item.co_crossline="{item}">
{{ co(item).crossline }}
</template>
<template v-slot:expanded-item="{ headers, item }">
<td :colspan="headers.length" class="pa-0">
<v-card flat>
<v-card-subtitle>Gun data</v-card-subtitle>
<v-card-text v-if="item.meta.raw.smsrc">
<v-container>
<v-row>
<v-col>
Source fired: {{ gun_data(item).src_number }}
</v-col>
<v-col>
Subarrays: {{ gun_data(item).num_subarray }}
</v-col>
<v-col>
Total guns: {{ gun_data(item).num_guns }}
</v-col>
<v-col>
Active guns: {{ gun_data(item).num_active }}
</v-col>
<v-col>
Autofires: {{ gun_data(item).num_auto }}
</v-col>
<v-col>
No fires: {{ gun_data(item).num_nofire }}
</v-col>
</v-row>
<v-row>
<v-col>
Average delta: {{ gun_data(item).avg_delta }} ±{{ gun_data(item).std_delta }} ms
</v-col>
<v-col>
Delta errors: {{ gun_data(item).num_delta }}
</v-col>
<v-col>
Source volume: {{ gun_data(item).volume }} in³
</v-col>
<v-col>
Manifold pressure: {{ gun_data(item).manifold }} psi
</v-col>
<v-col>
Mask: {{ gun_data(item).mask }}
</v-col>
<v-col>
Trigger mode: {{ gun_data(item).trg_mode }}
</v-col>
</v-row>
<v-row>
<v-col cols=12>
<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-header>
Individual gun data
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-simple-table>
<template v-slot:default>
<thead>
<th>String</th>
<th>Gun</th>
<th>Source</th>
<th>Mode</th>
<th>Detect</th>
<th>Autofire</th>
<th>Aimpoint</th>
<th>Fire time</th>
<th>Delay</th>
<th>Depth</th>
<th>Pressure</th>
<th>Volume</th>
<th>Filltime</th>
</thead>
<tbody>
<tr v-for="(gun_values, idx_gun) in gun_data(item).guns"
:key="idx_gun"
>
<td v-for="(val, idx) in gun_values" v-if="idx != 6"
:key="idx"
:class="typeof val === 'number' ? 'text-right' : 'text-left'"
>{{ gun_value_formatter(val, idx) }}</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-text v-else>
No gun data available {{ item }}
</v-card-text>
</v-card>
</td>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-card-text>
</v-card>
</v-container>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
export default {
name: "SequenceSummary"
name: "SequenceSummary",
data () {
return {
filter: null,
loading: false,
shotlogHeaders: [
{
value: "point",
text: "Point",
sort: this.sorterFor("point", this.number_sort)
},
{
value: "sailline",
text: "Sail line",
sort: this.sorterFor("sailline", this.number_sort)
},
{
value: "line",
text: "Source line",
sort: this.sorterFor("line", this.number_sort)
},
{
value: "tstamp",
text: "Timestamp",
sort: this.sorterFor("tstamp", this.string_sort)
},
{
value: "position",
text: "φ, λ",
sort: this.sorterFor(["geometryfinal", "geometryraw", "geometrypreplot"],
this.sorterFor("coordinates",
this.sequentialSort([0, 1], this.number_sort)))
},
{
value: "ε_inline",
text: "ε inline",
align: "end",
sort: this.sorterFor(["errorfinal", "errorraw"],
this.sorterFor("coordinates",
this.sorterFor(1, this.number_sort)))
},
{
value: "ε_crossline",
text: "ε crossline",
align: "end",
sort: this.sorterFor(["errorfinal", "errorraw"],
this.sorterFor("coordinates",
this.sorterFor(0, this.number_sort)))
},
{
value: "co_inline",
text: "c-o inline",
align: "end",
sort: (a, b) => this.number_sort(this.co(a).inline, this.co(b).inline)
},
{
value: "co_crossline",
text: "c-o crossline",
align: "end",
sort: (a, b) => this.number_sort(this.co(a).crossline, this.co(b).crossline)
},
{
value: 'data-table-expand'
},
],
shots: []
};
},
computed: {
sequenceNumber () {
return this.$route.params.sequence;
},
sequence () {
return this.sequences.find(i => i.sequence == this.sequenceNumber);
},
remarks () {
return this.sequence?.remarks || "Nil.";
},
remarks_final () {
return this.sequence?.remarks_final || "Nil.";
},
statusColour () {
switch (this.sequence?.status) {
case "final":
return "green--text";
case "raw":
return "orange--text";
case "ntbp":
return "red--text";
default:
return "";
}
},
infoColumns () {
return [
{
text: "FSP",
title: "First shotpoint",
value: this.sequence.fsp ?? "n/a"
},
{
text: "FPSP",
title: "First prime shotpoint",
value: this.sequence.fsp_final ?? "n/a"
},
{
text: "LPSP",
title: "Last prime shotpoint",
value: this.sequence.lsp_final ?? "n/a"
},
{
text: "LSP",
title: "Last shotpoint",
value: this.sequence.lsp ?? "n/a"
},
{
text: "Start time",
value: this.sequence.ts0,
},
{
text: "FPSP time",
title: "First prime shotpoint time",
value: this.sequence.ts0_final ?? "n/a",
},
{
text: "LPSP time",
title: "Last prime shotpoint time",
value: this.sequence.ts1_final ?? "n/a",
},
{
text: "End time",
value: this.sequence.ts1,
},
{
text: "Length",
value: this.sequence.length.toFixed(0)+" m"
},
{
text: "Azimuth",
value: this.sequence.azimuth.toFixed(4).padStart(8, "0")+"°"
},
{
text: "Shots acquired",
value: this.sequence.num_points
},
{
text: "Shots missed",
value: this.sequence.missing_shots
},
{
text: "Total duration",
value: this.format_duration(this.sequence.duration)
},
{
text: "Prime duration",
value: this.format_duration(this.sequence.duration_final) ?? "n/a"
}
];
},
...mapGetters(["sequencesLoading", "sequences"])
},
watch: {
sequencesLoading () {
this.loading == this.loading || this.sequencesLoading;
}
},
methods: {
/** Apply fn to a[keys], b[keys]
*
* If keys is an array, use the first
* attribute that exists in each of a
* and b (may not be the same attribute)
*/
sorterFor (keys, fn) {
if (Array.isArray(keys)) {
return (a, b) => fn(a[keys.find(k => k in a)], b[keys.find(k => k in b)]);
} else {
return (a, b) => fn(a[keys], b[keys]);
}
},
/** Apply fn to each member of keys
* until the comparison returns non-zero.
*/
sequentialSort (keys, fn) {
return (a, b) => {
for (const key of keys) {
const res = fn(a[key], b[key]);
if (res != 0) {
return res;
}
}
return 0;
}
},
number_sort (a, b) {
return a - b;
},
string_sort (a, b) {
return a > b
? 1
: a < b
? -1
: 0;
},
customSorter (items, sortBy, sortDesc) {
/** Get the sorter for a given column from shotlogHeaders
*/
const getFieldSorter = (key) => {
return this.shotlogHeaders.find(i => i.value == key)?.sort;
}
/** Conditionally invert a comparator
*/
function inverter (fn, desc) {
return desc
? (a, b) => fn(a, b) * -1
: fn;
}
for (const idx in sortBy) {
const sortKey = sortBy[idx];
const desc = sortDesc[idx];
const fn = inverter(getFieldSorter(sortKey), desc);
items.sort(fn);
}
return items;
},
onRowClicked (data, row) {
row.expand(!row.isExpanded);
},
async getSequence (seq = this.sequence) {
// NOTE:
// We are intentionally not using Vuex here, given that at this time
// no other component (except Graphs?) this endpoint's data.
// NB: Graphs does use this endpoint but there doesn't seem to be
// any point in keeping the data in memory anyway in case the user
// decides to navigate from the shotlog to the graphs or vice-versa.
try {
this.loading = true;
const url = `/project/${this.$route.params.project}/sequence/${this.$route.params.sequence}`;
this.shots = await this.api([url])
} catch {
} finally {
this.loading = false;
}
},
format_duration (duration) {
if (duration) {
let t = ["hours", "minutes", "seconds"].map(k =>
(duration[k] ?? 0).toFixed(0).padStart(2, "0")).join(":");
if (duration.days) {
t = duration.days+" d "+t;
}
return t;
}
},
format_position (position) {
return `${position.latitude.toFixed(6).padStart(9, "0")}, ${position.longitude.toFixed(6).padStart(10, "0")}`;
},
position (item) {
const coords = (item.geometryfinal ?? item.geometryraw)?.coordinates;
if (coords) {
return {
longitude: coords[0],
latitude: coords[1]
};
}
return {};
},
ε (item) {
const coords = (item.errorfinal ?? item.errorraw)?.coordinates;
if (coords) {
return {
crossline: coords[0]?.toFixed(2),
inline: coords[1]?.toFixed(2)
}
}
return {};
},
co (item) {
const raw = item.errorraw?.coordinates;
const final = item.errorfinal?.coordinates;
if (raw && final) {
return {
crossline: (final[0] - raw[0]).toFixed(2),
inline: (final[1] - raw[1]).toFixed(2),
}
} else {
return {
crossline: "n/a",
inline: "n/a"
}
}
},
gun_data (item) {
return item?.meta?.raw?.smsrc ?? {};
},
gun_value_formatter (value, index) {
const ms = (v) => v.toFixed(2)+" ms";
const transformers = [];
transformers[7] = ms;
transformers[8] = ms;
transformers[9] = ms;
transformers[10] = (v) => v.toFixed(1)+" m";
transformers[11] = (v) => v+" psi";
transformers[12] = (v) => v+" in³";
transformers[13] = ms;
const transformer = transformers[index];
return transformer ? transformer(value) : value;
},
...mapActions(["api"])
},
mounted () {
this.getSequence();
}
}
</script>