mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:57:08 +00:00
Compare commits
30 Commits
244-add-co
...
265-add-sh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf887b7852 | ||
|
|
c201229891 | ||
|
|
7ac997cd7d | ||
|
|
08e6c4a2de | ||
|
|
2c21f8f7ef | ||
|
|
a76aefe418 | ||
|
|
8d825fc53b | ||
|
|
b039a5f1fd | ||
|
|
5c1218e95e | ||
|
|
1bb5e2a41d | ||
|
|
1576b121e6 | ||
|
|
a06cdde449 | ||
|
|
121131e910 | ||
|
|
9136e9655d | ||
|
|
c646944886 | ||
|
|
0e664fc095 | ||
|
|
1498891004 | ||
|
|
89cb237f8d | ||
|
|
3386c57670 | ||
|
|
7285de5ec4 | ||
|
|
a95059f5e5 | ||
|
|
1ac81c34ce | ||
|
|
22387ba215 | ||
|
|
b77d41e952 | ||
|
|
aeecb7db7d | ||
|
|
ac9a683135 | ||
|
|
17a58f1396 | ||
|
|
b2a97a1987 | ||
|
|
f684e3e8d6 | ||
|
|
219425245f |
14
lib/www/client/source/package-lock.json
generated
14
lib/www/client/source/package-lock.json
generated
@@ -18,7 +18,7 @@
|
||||
"leaflet-realtime": "^2.2.0",
|
||||
"leaflet.markercluster": "^1.4.1",
|
||||
"marked": "^2.0.3",
|
||||
"plotly.js-dist": "^2.5.0",
|
||||
"plotly.js-dist": "^2.27.0",
|
||||
"suncalc": "^1.8.0",
|
||||
"typeface-roboto": "0.0.75",
|
||||
"vue": "^2.6.12",
|
||||
@@ -10317,9 +10317,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/plotly.js-dist": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.11.1.tgz",
|
||||
"integrity": "sha512-TubG71bBueWRMkQQMGlG8/0q753x5LLXzEHieUt9s0M/nD7WpnjC7sEwH+flhN6dOLqQ2wGlOb8Z4A3ah4IDWg=="
|
||||
"version": "2.27.0",
|
||||
"resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.27.0.tgz",
|
||||
"integrity": "sha512-SuuIF6zpJpW+9ssgXghMdcaJ9mp06SATykpxBVSrX9PpX9YZpCg6oo/79WvjYUVHf33QreJ4csAJU3o5Ll32cQ=="
|
||||
},
|
||||
"node_modules/pnp-webpack-plugin": {
|
||||
"version": "1.7.0",
|
||||
@@ -23254,9 +23254,9 @@
|
||||
}
|
||||
},
|
||||
"plotly.js-dist": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.11.1.tgz",
|
||||
"integrity": "sha512-TubG71bBueWRMkQQMGlG8/0q753x5LLXzEHieUt9s0M/nD7WpnjC7sEwH+flhN6dOLqQ2wGlOb8Z4A3ah4IDWg=="
|
||||
"version": "2.27.0",
|
||||
"resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.27.0.tgz",
|
||||
"integrity": "sha512-SuuIF6zpJpW+9ssgXghMdcaJ9mp06SATykpxBVSrX9PpX9YZpCg6oo/79WvjYUVHf33QreJ4csAJU3o5Ll32cQ=="
|
||||
},
|
||||
"pnp-webpack-plugin": {
|
||||
"version": "1.7.0",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"leaflet-realtime": "^2.2.0",
|
||||
"leaflet.markercluster": "^1.4.1",
|
||||
"marked": "^2.0.3",
|
||||
"plotly.js-dist": "^2.5.0",
|
||||
"plotly.js-dist": "^2.27.0",
|
||||
"suncalc": "^1.8.0",
|
||||
"typeface-roboto": "0.0.75",
|
||||
"vue": "^2.6.12",
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -3,6 +3,7 @@
|
||||
<v-app-bar
|
||||
app
|
||||
clipped-left
|
||||
elevation="1"
|
||||
>
|
||||
<v-img src="/wgp-logo.png"
|
||||
contain
|
||||
|
||||
@@ -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) {
|
||||
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] );
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import Vuex from 'vuex'
|
||||
import api from './modules/api'
|
||||
import user from './modules/user'
|
||||
import snack from './modules/snack'
|
||||
import projects from './modules/projects'
|
||||
import project from './modules/project'
|
||||
import event from './modules/event'
|
||||
import label from './modules/label'
|
||||
@@ -19,6 +20,7 @@ export default new Vuex.Store({
|
||||
api,
|
||||
user,
|
||||
snack,
|
||||
projects,
|
||||
project,
|
||||
event,
|
||||
label,
|
||||
|
||||
120
lib/www/client/source/src/store/modules/projects/actions.js
Normal file
120
lib/www/client/source/src/store/modules/projects/actions.js
Normal file
@@ -0,0 +1,120 @@
|
||||
|
||||
/** Fetch projects from server
|
||||
*/
|
||||
async function refreshProjects ({commit, dispatch, state, rootState}) {
|
||||
|
||||
if (state.loading) {
|
||||
commit('abortProjectsLoading');
|
||||
}
|
||||
|
||||
commit('setProjectsLoading');
|
||||
const pid = rootState.project.projectId;
|
||||
const url = `/project`;
|
||||
const init = {
|
||||
signal: state.loading.signal
|
||||
};
|
||||
const res = await dispatch('api', [url, init]);
|
||||
|
||||
if (res) {
|
||||
commit('setProjects', res);
|
||||
commit('setProjectsTimestamp');
|
||||
}
|
||||
commit('clearProjectsLoading');
|
||||
}
|
||||
|
||||
/** Return a subset of projects from state.projects
|
||||
*/
|
||||
async function getProjects ({commit, dispatch, state}, [{pid, name, schema, group, sortBy, sortDesc, itemsPerPage, page, text}] = [{}]) {
|
||||
let filteredProjects = [...state.projects];
|
||||
|
||||
if (sortBy) {
|
||||
|
||||
sortBy.forEach( (key, idx) => {
|
||||
filteredProjects.sort( (el0, el1) => {
|
||||
const a = el0?.[key];
|
||||
const b = el1?.[key];
|
||||
if (a < b) {
|
||||
return -1;
|
||||
} else if (a > b) {
|
||||
return 1;
|
||||
} else if (a == b) {
|
||||
return 0;
|
||||
} else if (a && !b) {
|
||||
return 1;
|
||||
} else if (!a && b) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
if (sortDesc && sortDesc[idx] === true) {
|
||||
filteredProjects.reverse();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (pid) {
|
||||
filteredProjects = filteredProjects.filter( project => project.pid == pid );
|
||||
}
|
||||
|
||||
if (name) {
|
||||
filteredProjects = filteredProjects.filter( project => project.name.toLowerCase().includes(name.toLowerCase()) );
|
||||
}
|
||||
|
||||
if (schema) {
|
||||
filteredProjects = filteredProjects.filter( project => project.schema.toLowerCase().includes(schema.toLowerCase()) );
|
||||
}
|
||||
|
||||
if (group) {
|
||||
filteredProjects = filteredProjects.filter( project => project.groups.find(g => g.toLowerCase() == group.toLowerCase()) );
|
||||
}
|
||||
|
||||
if (text) {
|
||||
const tstampFilter = (value, search, item) => {
|
||||
return search?.length >= 5 && textFilter(value, search, item);
|
||||
};
|
||||
|
||||
const numberFilter = (value, search, item) => {
|
||||
return value == search;
|
||||
};
|
||||
|
||||
const textFilter = (value, search, item) => {
|
||||
return String(value).toLowerCase().includes(search.toLowerCase());
|
||||
};
|
||||
|
||||
const arrayFilter = (elementFilter) => {
|
||||
return (value, search, item) => value.some(element => elementFilter(element, search, item));
|
||||
};
|
||||
|
||||
const searchFunctions = {
|
||||
pid: textFilter,
|
||||
name: textFilter,
|
||||
group: arrayFilter(textFilter)
|
||||
};
|
||||
|
||||
filteredProjects = filteredProjects.filter ( project => {
|
||||
for (let key in searchFunctions) {
|
||||
const fn = searchFunctions[key];
|
||||
if (fn(project[key], text, project)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
const count = filteredProjects.length;
|
||||
|
||||
if (itemsPerPage && itemsPerPage > 0) {
|
||||
const offset = (page > 0)
|
||||
? (page-1) * itemsPerPage
|
||||
: 0;
|
||||
|
||||
filteredProjects = filteredProjects.slice(offset, offset+itemsPerPage);
|
||||
}
|
||||
|
||||
return {projects: filteredProjects, count};
|
||||
}
|
||||
|
||||
export default { refreshProjects, getProjects };
|
||||
18
lib/www/client/source/src/store/modules/projects/getters.js
Normal file
18
lib/www/client/source/src/store/modules/projects/getters.js
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
function projects (state) {
|
||||
return state.projects;
|
||||
}
|
||||
|
||||
function projectGroups (state) {
|
||||
return [...new Set(state.projects.map(i => i.groups).flat())].sort();
|
||||
}
|
||||
|
||||
function projectCount (state) {
|
||||
return state.projects?.length ?? 0;
|
||||
}
|
||||
|
||||
function projectsLoading (state) {
|
||||
return !!state.loading;
|
||||
}
|
||||
|
||||
export default { projects, projectGroups, projectCount, projectsLoading };
|
||||
@@ -0,0 +1,6 @@
|
||||
import state from './state'
|
||||
import getters from './getters'
|
||||
import actions from './actions'
|
||||
import mutations from './mutations'
|
||||
|
||||
export default { state, getters, actions, mutations };
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
function setProjects (state, projects) {
|
||||
// We don't need or want the array to be reactive
|
||||
state.projects = Object.freeze(projects);
|
||||
}
|
||||
|
||||
function setProjectsLoading (state, abortController = new AbortController()) {
|
||||
state.loading = abortController;
|
||||
}
|
||||
|
||||
// This assumes that we know any transactions have finished or we
|
||||
// don't care about aborting.
|
||||
function clearProjectsLoading (state) {
|
||||
state.loading = null;
|
||||
}
|
||||
|
||||
function setProjectsTimestamp (state, timestamp = new Date()) {
|
||||
// NOTE: There is no `modified_on` property in the projects
|
||||
// result or in the database schema, but we should probably add
|
||||
// one.
|
||||
if (timestamp === true) {
|
||||
const tstamp = state.projects
|
||||
.map( event => event.modified_on )
|
||||
.reduce( (acc, cur) => acc > cur ? acc : cur );
|
||||
state.timestamp = tstamp ? new Date(tstamp) : new Date();
|
||||
} else {
|
||||
state.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
function setProjectsETag (state, etag) {
|
||||
state.etag = etag;
|
||||
}
|
||||
|
||||
function abortProjectsLoading (state) {
|
||||
if (state.loading) {
|
||||
state.loading.abort();
|
||||
}
|
||||
state.loading = null;
|
||||
}
|
||||
|
||||
export default {
|
||||
setProjects,
|
||||
setProjectsLoading,
|
||||
clearProjectsLoading,
|
||||
setProjectsTimestamp,
|
||||
setProjectsETag
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
const state = () => ({
|
||||
projects: Object.freeze([]),
|
||||
loading: null,
|
||||
timestamp: null,
|
||||
etag: null,
|
||||
});
|
||||
|
||||
export default state;
|
||||
@@ -37,7 +37,65 @@
|
||||
</v-btn>
|
||||
<v-toolbar-title v-if="$refs.calendar">
|
||||
{{ $refs.calendar.title }}
|
||||
|
||||
<span
|
||||
class="secondary--text"
|
||||
style="font-size:small;"
|
||||
>
|
||||
({{downloadableItemCount}} log entries)
|
||||
</span>
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-menu class="ml-5" v-if="calendarDates">
|
||||
<template v-slot:activator="{on, attrs}">
|
||||
<v-btn
|
||||
class="ml-5"
|
||||
small
|
||||
:title="`Download events for the period ${calendarDates.start} ‒ ${calendarDates.end}`"
|
||||
v-on="on"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<span class="d-none d-lg-inline">Download as…</span>
|
||||
<v-icon right small>mdi-cloud-download</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item
|
||||
:href="downloadUrl({mime: 'application/vnd.seis+json', filename: `event-log-${calendarDates.start}-${calendarDates.end}-multiseis.json`})"
|
||||
title="Download as a Multiseis-compatible Seis+JSON file."
|
||||
>Seis+JSON</v-list-item>
|
||||
<!-- Not yet supported
|
||||
<v-list-item
|
||||
:href="downloadUrl({mime: 'application/geo+json'})"
|
||||
title="Download as a QGIS-compatible GeoJSON file"
|
||||
>GeoJSON</v-list-item>
|
||||
-->
|
||||
<v-list-item
|
||||
:href="downloadUrl({mime: 'application/json', filename: `event-log-${calendarDates.start}-${calendarDates.end}.json`})"
|
||||
title="Download as a generic JSON file"
|
||||
>JSON</v-list-item>
|
||||
<v-list-item
|
||||
:href="downloadUrl({mime: 'application/yaml', filename: `event-log-${calendarDates.start}-${calendarDates.end}.yaml`})"
|
||||
title="Download as a YAML file"
|
||||
>YAML</v-list-item>
|
||||
<v-list-item
|
||||
:href="downloadUrl({mime: 'text/csv', sortBy: 'tstamp', sortDesc: false, filename: `event-log-${calendarDates.start}-${calendarDates.end}.csv`})"
|
||||
title="Download as Comma Separated Values file"
|
||||
>CSV</v-list-item>
|
||||
<!-- Not yet supportd
|
||||
<v-list-item
|
||||
:href="downloadUrl({mime: 'text/html'})"
|
||||
title="Download as an HTML formatted file"
|
||||
>HTML</v-list-item>
|
||||
<v-list-item
|
||||
:href="downloadUrl({mime: 'application/pdf'})"
|
||||
title="Download as a Portable Document File"
|
||||
>PDF</v-list-item>
|
||||
-->
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="categoriesAvailable"
|
||||
small
|
||||
@@ -169,6 +227,23 @@ export default {
|
||||
return [...new Set(this.visibleItems.map(i => i.category ?? "General"))];
|
||||
},
|
||||
|
||||
calendarDates () {
|
||||
// The this.items.length reference is only needed to force recalculation
|
||||
// of this computed property, as this.$refs is not reactive.
|
||||
// https://github.com/vuejs/vue/issues/3842
|
||||
if (this.items.length && this.$refs.calendar) {
|
||||
return {
|
||||
start: this.$refs.calendar.renderProps.start.date,
|
||||
end: this.$refs.calendar.renderProps.end.date
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
downloadableItemCount () {
|
||||
return this.events.filter(i => i.tstamp.substr(0, 10) >= this.calendarDates?.start &&
|
||||
i.tstamp.substr(0, 10) <= this.calendarDates?.end).length;
|
||||
},
|
||||
|
||||
...mapGetters(['sequencesLoading', 'sequences', 'events'])
|
||||
},
|
||||
|
||||
@@ -298,6 +373,18 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
downloadUrl (qry) {
|
||||
if (this.calendarDates) {
|
||||
const url = new URL(`/api/project/${this.$route.params.project}/event`, document.location.href);
|
||||
for (const key in qry) {
|
||||
url.searchParams.set(key, qry[key]);
|
||||
}
|
||||
url.searchParams.set("date0", this.calendarDates.start);
|
||||
url.searchParams.set("date1", this.calendarDates.end);
|
||||
return url.toString();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
...mapActions(["api"])
|
||||
|
||||
|
||||
@@ -33,6 +33,17 @@
|
||||
</span>
|
||||
</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"
|
||||
v-model="eventDialog"
|
||||
v-bind="editedEvent"
|
||||
@@ -86,6 +97,49 @@
|
||||
>PDF</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-menu v-else>
|
||||
<template v-slot:activator="{on, attrs}">
|
||||
<v-btn class="ml-5" small v-on="on" v-bind="attrs">
|
||||
<span class="d-none d-lg-inline">Download as…</span>
|
||||
<v-icon right small>mdi-cloud-download</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event?mime=application%2Fvnd.seis%2Bjson&filename=event-log-multiseis.json`"
|
||||
title="Download as a Multiseis-compatible Seis+JSON file."
|
||||
>Seis+JSON</v-list-item>
|
||||
<!-- Not yet implemented
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event?mime=application%2Fgeo%2Bjson&filename=event-log.geojson`"
|
||||
title="Download as a QGIS-compatible GeoJSON file"
|
||||
>GeoJSON</v-list-item>
|
||||
-->
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event?mime=application%2Fjson&filename=event-log.json`"
|
||||
title="Download as a generic JSON file"
|
||||
>JSON</v-list-item>
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event?mime=application%2Fyaml&filename=event-log.yaml`"
|
||||
title="Download as a YAML file"
|
||||
>YAML</v-list-item>
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event?mime=text%2Fcsv&sortBy=tstamp&sortDesc=false&filename=event-log.csv`"
|
||||
title="Download as Comma Separated Values file"
|
||||
>CSV</v-list-item>
|
||||
<!-- Not yet implemented
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event?mime=text%2Fhtml&filename=event-log.html`"
|
||||
title="Download as an HTML formatted file"
|
||||
>HTML</v-list-item>
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event?mime=application%2Fpdf&filename=event-log.pdf`"
|
||||
title="Download as a Portable Document File"
|
||||
>PDF</v-list-item>
|
||||
-->
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
@@ -480,7 +534,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'writeaccess', 'eventsLoading', 'online', 'sequence', 'line', 'point', 'position', 'timestamp', 'lineName', 'serverEvent', 'events', 'labels', 'userLabels']),
|
||||
...mapGetters(['user', 'writeaccess', 'eventsLoading', 'online', 'sequence', 'line', 'point', 'position', 'timestamp', 'lineName', 'events', 'labels', 'userLabels', 'projectConfiguration']),
|
||||
...mapState({projectSchema: state => state.project.projectSchema})
|
||||
|
||||
},
|
||||
@@ -608,7 +662,6 @@ export default {
|
||||
this.editedEvent.longitude = template.meta.geometry.coordinates[0];
|
||||
this.editedEvent.latitude = template.meta.geometry.coordinates[1];
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/** Add a brand new event.
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
</template>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<template v-slot:item.shots="{item}">
|
||||
{{ item.total ? (item.prime + item.other) : "" }}
|
||||
</template>
|
||||
@@ -112,13 +110,14 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['loading', 'serverEvent'])
|
||||
|
||||
displayItems () {
|
||||
return this.showArchived
|
||||
? this.items
|
||||
: this.items.filter(i => !i.archived);
|
||||
},
|
||||
|
||||
...mapGetters(['loading', 'serverEvent', 'adminaccess', 'projects'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
@@ -332,9 +332,31 @@
|
||||
</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>
|
||||
<div style="white-space:nowrap;">
|
||||
{{value}}
|
||||
|
||||
<a
|
||||
:href="`/projects/${$route.params.project}/log/sequence/${value}`"
|
||||
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 v-slot:item.line="{value}">
|
||||
|
||||
@@ -1,13 +1,631 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<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 {
|
||||
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>
|
||||
|
||||
@@ -7,10 +7,10 @@ module.exports = {
|
||||
host: "0.0.0.0",
|
||||
proxy: {
|
||||
"^/api(/|$)": {
|
||||
target: "http://localhost:3000",
|
||||
target: "http://127.0.0.1:3000",
|
||||
},
|
||||
"^/ws(/|$)": {
|
||||
target: "ws://localhost:3000",
|
||||
target: "ws://127.0.0.1:3000",
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const yaml = require('./yaml');
|
||||
const geojson = require('./geojson');
|
||||
const seis = require('./seis');
|
||||
const csv = require('./csv');
|
||||
const setContentDisposition = require('../../../../lib/utils/setContentDisposition');
|
||||
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
@@ -19,6 +20,7 @@ module.exports = async function (req, res, next) {
|
||||
|
||||
if (mimetype) {
|
||||
res.set("Content-Type", mimetype);
|
||||
setContentDisposition(req, res);
|
||||
await handlers[mimetype](req, res, next);
|
||||
} else {
|
||||
res.status(406).send();
|
||||
|
||||
@@ -8,5 +8,6 @@ module.exports = {
|
||||
removeNulls: require('./removeNulls'),
|
||||
logicalPath: require('./logicalPath'),
|
||||
ranges: require('./ranges'),
|
||||
unique: require('./unique')
|
||||
unique: require('./unique'),
|
||||
setContentDisposition: require('./setContentDisposition')
|
||||
};
|
||||
|
||||
8
lib/www/server/lib/utils/setContentDisposition.js
Normal file
8
lib/www/server/lib/utils/setContentDisposition.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
function setContentDisposition (req, res) {
|
||||
if (req.query.filename) {
|
||||
res.set("Content-Disposition", `attachment; filename="${req.query.filename}"`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = setContentDisposition;
|
||||
@@ -216,6 +216,26 @@ components:
|
||||
pattern: "(([^\\s,;:]+)(\\s*[,;:\\s]\\s*)?)+"
|
||||
example: "line,point,tstamp"
|
||||
|
||||
QueryMime:
|
||||
name: mime
|
||||
description: |
|
||||
Ask for the representation of the requested resource to be provided in the indicated MIME type. Overrides the [`Accept`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) request header values (if any).
|
||||
|
||||
Note that the server is not obliged to honour the requested type and a default (usually `application/json`) might be provided instead. Always check the response's [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) header.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
|
||||
QueryFilename:
|
||||
name: filename
|
||||
description: |
|
||||
Cause the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) response header to be set and suggest a file name to download as.
|
||||
|
||||
Note that the [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) response header is not affected by this parameter.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
|
||||
CSVDelimiter:
|
||||
description: Delimiter value to use in CSV requests or responses.
|
||||
name: delimiter
|
||||
@@ -1394,6 +1414,8 @@ paths:
|
||||
- $ref: "#/components/parameters/CSVDelimiter"
|
||||
- $ref: "#/components/parameters/CSVFields"
|
||||
- $ref: "#/components/parameters/CSVHeader"
|
||||
- $ref: "#/components/parameters/QueryMime"
|
||||
- $ref: "#/components/parameters/QueryFilename"
|
||||
responses:
|
||||
"200":
|
||||
description: List of project events
|
||||
|
||||
Reference in New Issue
Block a user