Merge branch '265-add-shotlog' into 'devel'

Resolve "Add shotlog"

Closes #265

See merge request wgp/dougal/software!54
This commit is contained in:
D. Berge
2023-10-31 18:51:16 +00:00
8 changed files with 1183 additions and 17 deletions

View File

@@ -18,7 +18,7 @@
"leaflet-realtime": "^2.2.0", "leaflet-realtime": "^2.2.0",
"leaflet.markercluster": "^1.4.1", "leaflet.markercluster": "^1.4.1",
"marked": "^2.0.3", "marked": "^2.0.3",
"plotly.js-dist": "^2.5.0", "plotly.js-dist": "^2.27.0",
"suncalc": "^1.8.0", "suncalc": "^1.8.0",
"typeface-roboto": "0.0.75", "typeface-roboto": "0.0.75",
"vue": "^2.6.12", "vue": "^2.6.12",
@@ -10317,9 +10317,9 @@
} }
}, },
"node_modules/plotly.js-dist": { "node_modules/plotly.js-dist": {
"version": "2.11.1", "version": "2.27.0",
"resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.11.1.tgz", "resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.27.0.tgz",
"integrity": "sha512-TubG71bBueWRMkQQMGlG8/0q753x5LLXzEHieUt9s0M/nD7WpnjC7sEwH+flhN6dOLqQ2wGlOb8Z4A3ah4IDWg==" "integrity": "sha512-SuuIF6zpJpW+9ssgXghMdcaJ9mp06SATykpxBVSrX9PpX9YZpCg6oo/79WvjYUVHf33QreJ4csAJU3o5Ll32cQ=="
}, },
"node_modules/pnp-webpack-plugin": { "node_modules/pnp-webpack-plugin": {
"version": "1.7.0", "version": "1.7.0",
@@ -23254,9 +23254,9 @@
} }
}, },
"plotly.js-dist": { "plotly.js-dist": {
"version": "2.11.1", "version": "2.27.0",
"resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.11.1.tgz", "resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.27.0.tgz",
"integrity": "sha512-TubG71bBueWRMkQQMGlG8/0q753x5LLXzEHieUt9s0M/nD7WpnjC7sEwH+flhN6dOLqQ2wGlOb8Z4A3ah4IDWg==" "integrity": "sha512-SuuIF6zpJpW+9ssgXghMdcaJ9mp06SATykpxBVSrX9PpX9YZpCg6oo/79WvjYUVHf33QreJ4csAJU3o5Ll32cQ=="
}, },
"pnp-webpack-plugin": { "pnp-webpack-plugin": {
"version": "1.7.0", "version": "1.7.0",

View File

@@ -16,7 +16,7 @@
"leaflet-realtime": "^2.2.0", "leaflet-realtime": "^2.2.0",
"leaflet.markercluster": "^1.4.1", "leaflet.markercluster": "^1.4.1",
"marked": "^2.0.3", "marked": "^2.0.3",
"plotly.js-dist": "^2.5.0", "plotly.js-dist": "^2.27.0",
"suncalc": "^1.8.0", "suncalc": "^1.8.0",
"typeface-roboto": "0.0.75", "typeface-roboto": "0.0.75",
"vue": "^2.6.12", "vue": "^2.6.12",

View File

@@ -0,0 +1,290 @@
<template>
<div ref="graph"
class="graph-container"
></div>
</template>
<style scoped>
.graph-container {
width: 100%;
height: 100%;
}
</style>
<script>
import Plotly from 'plotly.js-dist';
import unpack from '@/lib/unpack.js';
export default {
name: "DougalGraphProjectSequenceInlineCrossline",
props: {
items: Array,
gunDataFormat: { type: String, default: "smsrc" },
facet: { type: String, default: "scatter" }
},
data () {
return {
plotted: false,
resizeObserver: null
};
},
computed: {
config () {
switch (this.facet) {
case "scatter":
default:
return {
editable: false,
displayLogo: false
};
}
},
layout () {
const base = {
font: {
color: this.$vuetify.theme.isDark ? "#fff" : undefined
}
};
switch (this.facet) {
case "scatter":
return {
...base,
autocolorscale: true,
title: {text: `Preplot deviation <span style="font-size:smaller;">(x̅: %{data[0].meta.avg_x} ±%{data[0].meta.std_x} m; y̅: %{data[0].meta.avg_y} ±%{data[0].meta.std_y} m)</span>`},
xaxis: {
title: "Crossline (m)"
},
yaxis: {
title: "Inline (m)"
},
plot_bgcolor:"rgba(0,0,0,0)",
paper_bgcolor:"rgba(0,0,0,0)"
};
case "crossline":
return {
...base,
autocolorscale: true,
title: {text: `Crossline deviation <span style="font-size:smaller;">(x̅: %{data[0].meta.avg_x} ±%{data[0].meta.std_x} m)</span>`},
xaxis: {
title: "Shotpoint"
},
yaxis: {
title: "Crossline (m)"
},
plot_bgcolor:"rgba(0,0,0,0)",
paper_bgcolor:"rgba(0,0,0,0)"
};
case "2dhist":
return {
...base,
showlegend: true,
title: {text: `Preplot deviation <span style="font-size:smaller;">(x̅: %{data[0].meta.avg_x} ±%{data[0].meta.std_x} m; y̅: %{data[0].meta.avg_y} ±%{data[0].meta.std_y} m)</span>`},
xaxis: {
title: "Crossline (m)",
showgrid: true,
zeroline: true
},
yaxis: {
title: "Inline (m)",
showgrid: true,
zeroline: true
},
plot_bgcolor:"rgba(0,0,0,0)",
paper_bgcolor:"rgba(0,0,0,0)"
};
case "c-o":
return {
...base,
showlegend: true,
title: {text: `Final vs raw <span style="font-size:smaller;">(x̅: %{data[0].meta.avg_x} ±%{data[0].meta.std_x} m; y̅: %{data[0].meta.avg_y} ±%{data[0].meta.std_y} m)</span>`},
xaxis: {
title: "Crossline (m)",
showgrid: true,
zeroline: true
},
yaxis: {
title: "Inline (m)",
showgrid: true,
zeroline: true
},
plot_bgcolor:"rgba(0,0,0,0)",
paper_bgcolor:"rgba(0,0,0,0)"
};
}
},
data () {
if (!this.items?.length) {
return [];
}
let x, y, avg_x, avg_y, std_x, std_y;
const items = this.items.sort( (a, b) => a.point - b.point );
const meta = unpack(items, "meta");
const src_number = unpack(unpack(unpack(meta, "raw"), this.gunDataFormat), "src_number");
if (this.facet == "c-o") {
const _items = items.filter(i => i.errorfinal && i.errorraw);
const εf = unpack(unpack(_items, "errorfinal"), "coordinates");
const εr = unpack(unpack(_items, "errorraw"), "coordinates");
x = εf.map( (f, idx) => f[0] - εr[idx][0] )
y = εf.map( (f, idx) => f[1] - εr[idx][1] )
} else {
const coords = unpack(unpack(items, ((row) => row?.errorfinal ? row.errorfinal : row.errorraw)), "coordinates");
x = unpack(coords, 0);
y = unpack(coords, 1);
}
// No chance of overflow
avg_x = (x.reduce((acc, cur) => acc + cur, 0) / x.length).toFixed(2);
avg_y = (y.reduce((acc, cur) => acc + cur, 0) / y.length).toFixed(2);
std_x = Math.sqrt(x.reduce((acc, cur) => (cur-avg_x)**2 + acc, 0) / x.length).toFixed(2);
std_y = Math.sqrt(y.reduce((acc, cur) => (cur-avg_y)**2 + acc, 0) / y.length).toFixed(2);
if (this.facet == "scatter") {
const data = [{
type: "scatter",
mode: "markers",
x,
y,
meta: { avg_x, avg_y, std_x, std_y},
transforms: [{
type: "groupby",
groups: src_number,
styles: [
{target: 1, value: {line: {color: "green"}}},
{target: 2, value: {line: {color: "red"}}},
{target: 3, value: {line: {color: "blue"}}}
]
}],
}];
return data;
} else if (this.facet == "crossline") {
const s = unpack(items, "point");
const data = [{
type: "scatter",
x: s,
y: x,
meta: { avg_x, avg_y, std_x, std_y},
_transforms: [{
type: "groupby",
groups: src_number,
styles: [
{target: 1, value: {line: {color: "green"}}},
{target: 2, value: {line: {color: "red"}}},
{target: 3, value: {line: {color: "blue"}}}
]
}],
}];
return data;
} else if (this.facet == "2dhist" || this.facet == "c-o") {
const bottomValue = this.$vuetify.theme.isDark
? ['0.0', 'rgba(0,0,0,0)']
: ['0.0', 'rgb(165,0,38)'];
const topValue = this.$vuetify.theme.isDark
? ['1.0', 'rgb(49,54,149)']
: ['1.0', 'rgba(0,0,0,0)'];
const colourscale = this.facet == "c-o"
? [bottomValue, [0.1, 'rgb(0,0,0)'], [0.9, 'rgb(255,255,255)'], topValue]
: [
bottomValue,
['0.111111111111', 'rgb(215,48,39)'],
['0.222222222222', 'rgb(244,109,67)'],
['0.333333333333', 'rgb(253,174,97)'],
['0.444444444444', 'rgb(254,224,144)'],
['0.555555555556', 'rgb(224,243,248)'],
['0.666666666667', 'rgb(171,217,233)'],
['0.777777777778', 'rgb(116,173,209)'],
['0.888888888889', 'rgb(69,117,180)'],
topValue
];
const data = [{
type: "histogram2dcontour",
ncontours: 20,
colorscale: colourscale,
showscale: false,
reversescale: !this.$vuetify.theme.isDark,
contours: {
coloring: this.facet == "c-o" ? "fill" : "heatmap",
},
x,
y,
meta: { avg_x, avg_y, std_x, std_y}
}];
return data;
}
}
},
watch: {
items (cur, prev) {
if (cur != prev) {
this.plot();
}
},
"$vuetify.theme.isDark" () {
this.plot();
}
},
methods: {
plot () {
Plotly.newPlot(this.$refs.graph, this.data, this.layout, this.config);
this.plotted = true;
},
replot () {
if (this.plotted) {
const ref = this.$refs.graph;
Plotly.relayout(ref, {
width: ref.clientWidth,
height: ref.clientHeight
});
}
}
},
mounted () {
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graph);
},
beforeDestroy () {
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.$refs.graph);
}
}
}
</script>

View File

@@ -0,0 +1,196 @@
<template>
<div ref="graph"
class="graph-container"
></div>
</template>
<style scoped>
.graph-container {
width: 100%;
height: 100%;
}
</style>
<script>
import Plotly from 'plotly.js-dist';
import unpack from '@/lib/unpack.js';
export default {
name: "DougalGraphProjectSequenceShotpointTiming",
props: {
items: Array,
gunDataFormat: { type: String, default: "smsrc" },
facet: { type: String, default: "bars" }
},
data () {
return {
plotted: false,
resizeObserver: null
};
},
computed: {
config () {
return {
editable: false,
displayLogo: false
};
},
layout () {
return {
font: {
color: this.$vuetify.theme.isDark ? "#fff" : undefined
},
title: {text: "Shotpoint timing %{data[0].meta.subtitle}"},
xaxis: {
title: "Shotpoint"
},
yaxis: {
title: "Time (s)"
},
plot_bgcolor:"rgba(0,0,0,0)",
paper_bgcolor:"rgba(0,0,0,0)"
};
},
data () {
const items = this.items.map(i => {
return {
point: i.point,
tstamp: new Date(i.tstamp)
}
}).sort( (a, b) => a.tstamp - b.tstamp );
const x = [...unpack(items, "point")];
const y = items.map( (i, idx, ary) => (ary[idx+1]?.tstamp - i.tstamp)/1000 );
const src_number = unpack(this.items, ["meta", "raw", this.gunDataFormat, "src_number"]);
// We're dealing with intervals not points
x.pop(); y.pop(); src_number.pop();
const meta = {};
const stats = this.stats(x, y, src_number);
// We need to do the subtitle here rather than in layout as layout knows nothing
// about the number of arrays
if (stats.src_ids.length == 1) {
meta.subtitle = `<span style="font-size:smaller;">(μ = ${stats.avg.all.toFixed(2)} ±${stats.std.all.toFixed(2)} s)</span>`;
} else {
meta.subtitle = `<span style="font-size:smaller;">(μ = ${stats.avg.all.toFixed(2)} ±${stats.std.all.toFixed(2)} s)</span>`;
const per_source = [];
for (const key in stats.avg) {
if (key == "all") continue;
const s = `μ<sub>${key}</sub> = ${stats.avg[key].toFixed(2)} ±${stats.std[key].toFixed(2)} s`;
per_source.push(s);
}
meta.subtitle += `<br><span style="font-size:smaller;">` + per_source.join("; ") + "</span>";
}
const trace0 = {
type: "bar",
x,
y,
transforms: [{
type: "groupby",
groups: src_number,
styles: [
{target: 1, value: {line: {color: "green"}}},
{target: 2, value: {line: {color: "red"}}},
{target: 3, value: {line: {color: "blue"}}}
]
}],
meta
};
switch (this.facet) {
case "lines":
trace0.type = "scatter";
break;
case "area":
trace0.type = "scatter";
trace0.fill = "tozeroy";
break;
case "bars":
default:
// Nothing
}
return [trace0]
}
},
watch: {
items (cur, prev) {
if (cur != prev) {
this.plot();
}
},
"$vuetify.theme.isDark" () {
this.plot();
}
},
methods: {
plot () {
Plotly.newPlot(this.$refs.graph, this.data, this.layout, this.config);
this.plotted = true;
},
replot () {
if (this.plotted) {
const ref = this.$refs.graph;
Plotly.relayout(ref, {
width: ref.clientWidth,
height: ref.clientHeight
});
}
},
stats (x, y, src_number) {
const avg = {};
const std = {};
const avg_all = (y.reduce((acc, cur) => acc + cur, 0) / y.length);
const std_all = Math.sqrt(y.reduce((acc, cur) => (cur-avg_all)**2 + acc, 0) / y.length);
avg.all = avg_all;
std.all = std_all;
const src_ids = new Set(src_number);
for (const src of src_ids) {
const v = y.filter((i, idx) => src_number[idx] == src);
const μ = (v.reduce((acc, cur) => acc + cur, 0) / v.length);
const σ = Math.sqrt(v.reduce((acc, cur) => (cur-μ)**2 + acc, 0) / v.length);
avg[src] = μ;
std[src] = σ;
}
return { avg, std, src_ids };
}
},
mounted () {
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graph);
},
beforeDestroy () {
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.$refs.graph);
}
}
}
</script>

View File

@@ -1,4 +1,33 @@
/** Unpacks attributes from array items.
*
* At it simplest, given an array of objects,
* the call unpack(rows, "x") returns an array
* of the "x" attribute of every item in rows.
*
* `key` may also be:
*
* - a function with the signature
* (Object) => any
* the result of applying the function to
* the object will be used as the unpacked
* value.
*
* - an array of strings, functions or other
* arrays. In this case, it does a recursive
* fold operation. NOTE: it mutates `key`.
*
*/
export default function unpack(rows, key) { export default function unpack(rows, key) {
return rows && rows.map( row => row[key] ); if (typeof key === "function") {
return rows && rows.map( row => key(row) );
} else if (Array.isArray(key)) {
const car = key.shift();
if (key.length) {
return unpack(unpack(rows, car), key);
} else {
return unpack(rows, car);
}
} else {
return rows && rows.map( row => row?.[key] );
}
}; };

View File

@@ -33,6 +33,17 @@
</span> </span>
</v-toolbar-title> </v-toolbar-title>
<a v-if="$route.params.sequence"
class="mr-3"
:href="`/projects/${$route.params.project}/sequences/${$route.params.sequence}`"
title="View the shotlog for this sequence"
>
<v-icon
right
color="teal"
>mdi-format-list-numbered</v-icon>
</a>
<dougal-event-edit v-if="writeaccess" <dougal-event-edit v-if="writeaccess"
v-model="eventDialog" v-model="eventDialog"
v-bind="editedEvent" v-bind="editedEvent"

View File

@@ -332,9 +332,31 @@
</template> </template>
<template v-slot:item.sequence="{value}"> <template v-slot:item.sequence="{value}">
<div style="white-space:nowrap;">
{{value}}
<a <a
:href="`/projects/${$route.params.project}/log/sequence/${value}`" :href="`/projects/${$route.params.project}/log/sequence/${value}`"
title="View the event log for this sequence">{{value}}</a> title="View the event log for this sequence"
>
<v-icon
small
right
color="blue"
>mdi-format-list-bulleted-type</v-icon>
</a>
<a
:href="`/projects/${$route.params.project}/sequences/${value}`"
title="View the shotlog for this sequence"
>
<v-icon
small
right
color="teal"
>mdi-format-list-numbered</v-icon>
</a>
</div>
</template> </template>
<template v-slot:item.line="{value}"> <template v-slot:item.line="{value}">

View File

@@ -1,13 +1,631 @@
<template> <template>
<div> <v-container fluid>
SequenceSummary
<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>
<a v-if="$route.params.sequence"
:href="`/projects/${$route.params.project}/log/sequence/${$route.params.sequence}`"
title="View the event log for this sequence"
>
<v-icon
right
color="blue"
>mdi-format-list-bulleted-type</v-icon>
</a>
<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-row>
<v-col cols=12 md=6 lg=4>
<div style="height:300px;">
<dougal-graph-project-sequence-inline-crossline
:items="shots"
gun-data-format="smsrc"
facet="2dhist"
>
</dougal-graph-project-sequence-inline-crossline>
</div> </div>
</v-col>
<v-col cols=12 md=6 lg=4>
<div style="height:300px;">
<dougal-graph-project-sequence-inline-crossline
:items="shots"
gun-data-format="smsrc"
facet="crossline"
>
</dougal-graph-project-sequence-inline-crossline>
</div>
</v-col>
<v-col cols=12 md=6 lg=4>
<div ref="" style="height:300px;">
<dougal-graph-project-sequence-shotpoint-timing
:items="shots"
facet="area"
>
</dougal-graph-project-sequence-shotpoint-timing>
</div>
</v-col>
<v-col cols=12 md=6 lg=4 >
<div ref="" style="height:300px;">
<dougal-graph-project-sequence-inline-crossline
:items="shots"
facet="c-o"
>
</dougal-graph-project-sequence-inline-crossline>
</div>
</v-col>
</v-row>
</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> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex';
import DougalGraphProjectSequenceInlineCrossline from '@/components/graphs/project/sequence/inline-crossline';
import DougalGraphProjectSequenceShotpointTiming from '@/components/graphs/project/sequence/shotpoint-timing';
export default { export default {
name: "SequenceSummary" name: "SequenceSummary",
components: {
DougalGraphProjectSequenceInlineCrossline,
DougalGraphProjectSequenceShotpointTiming
},
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> </script>