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

966 lines
29 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<v-container fluid>
<v-card>
<v-card-title>
<v-toolbar flat>
<v-toolbar-title>Plan</v-toolbar-title>
<v-menu v-if="items">
<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}/plan/?mime=text%2Fcsv&download`"
title="Download as a comma-separated values file."
>CSV</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/plan/?mime=application%2Fgeo%2Bjson&download`"
title="Download as a QGIS-compatible GeoJSON file"
>GeoJSON</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/plan/?mime=application%2Fjson&download`"
title="Download as a generic JSON file"
>JSON</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/plan/?mime=text%2Fhtml&download`"
title="Download as an HTML formatted file"
>HTML</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/plan/?mime=application%2Fpdf&download`"
title="Download as a Portable Document File"
>PDF</v-list-item>
</v-list>
</v-menu>
<v-spacer></v-spacer>
<v-text-field
v-model="filter"
append-icon="mdi-magnify"
label="Filter"
single-line
clearable
hint="Filter by sequence, line, first or last shotpoints, remarks or start/end time"
></v-text-field>
</v-toolbar>
</v-card-title>
<v-card-text>
<v-menu v-if="writeaccess"
v-model="contextMenuShow"
:position-x="contextMenuX"
:position-y="contextMenuY"
absolute
offset-y
>
<v-list dense v-if="contextMenuItem">
<v-list-item @click="deletePlannedSequence">
<v-list-item-icon><v-icon>mdi-delete</v-icon></v-list-item-icon>
<v-list-item-title class="warning--text">Delete planned sequence</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-card class="mb-5" flat>
<v-card-title class="text-overline">
Comments
<template v-if="writeaccess">
<v-btn v-if="!editRemarks"
class="ml-3"
small
icon
title="Edit comments"
@click="editRemarks=true"
>
<v-icon small>mdi-square-edit-outline</v-icon>
</v-btn>
<v-btn v-else
class="ml-3"
small
icon
title="Save comments"
@click="saveRemarks"
>
<v-icon>mdi-content-save-edit-outline</v-icon>
</v-btn>
</template>
</v-card-title>
<v-card-text v-if="editRemarks">
<v-textarea
v-model="remarks"
class="markdown"
placeholder="Plan comments"
dense
auto-grow
rows="1"
></v-textarea>
</v-card-text>
<v-card-text v-else v-html="$options.filters.markdown(remarks || '*(nil)*')"></v-card-text>
</v-card>
<v-data-table
:headers="headers"
:items="items"
:items-per-page.sync="itemsPerPage"
:server-items-length="sequenceCount"
item-key="sequence"
:search="filter"
:loading="plannedSequencesLoading"
fixed-header
no-data-text="No planned lines. Add lines via the context menu from either the Lines or Sequences view."
:item-class="(item) => (activeItem == item && !edit) ? 'blue accent-1 elevation-3' : ''"
:footer-props="{showFirstLastPage: true}"
@click:row="setActiveItem"
@contextmenu:row="contextMenu"
>
<template v-slot:item.srss="{item}">
<span style="white-space: nowrap;">
<v-icon small :title="srssInfo(item)">{{srssIcon(item)}}</v-icon>
/
<v-icon small :title="wxInfo(item)" v-if="item.meta.wx">{{wxIcon(item)}}</v-icon>
</span>
</template>
<template v-slot:item.sequence="{item, value}">
<v-edit-dialog v-if="writeaccess"
large
@open="editItem(item, 'sequence')"
@save="edit = null"
@cancel="edit.value = item.sequence; edit = null"
>
<span>{{ value }}</span>
<template v-slot:input>
<v-text-field v-if="edit"
type="number"
v-model.number="edit.value"
single-line
>
</v-text-field>
<v-checkbox
v-model="shiftAll"
class="mt-0"
label="Shift all planned sequences"
></v-checkbox>
</template>
</v-edit-dialog>
<span v-else>{{ value }}</span>
</template>
<template v-slot:item.name="{item, value}">
<v-edit-dialog v-if="writeaccess"
large
@open="editItem(item, 'name')"
@save="edit = null"
@cancel="edit.value = item.name; edit = null"
>
<span>{{ value }}</span>
<template v-slot:input>
<v-text-field v-if="edit"
v-model="edit.value"
single-line
>
</v-text-field>
</template>
</v-edit-dialog>
<span v-else>{{ value }}</span>
</template>
<template v-slot:item.fsp="{item, value}">
<v-edit-dialog v-if="writeaccess"
large
@open="editItem(item, 'fsp')"
@save="edit = null"
@cancel="edit.value = item.fsp; edit = null"
>
<span>{{ value }}</span>
<template v-slot:input>
<v-text-field v-if="edit"
type="number"
v-model.number="edit.value"
single-line
>
</v-text-field>
</template>
</v-edit-dialog>
<span v-else>{{ value }}</span>
</template>
<template v-slot:item.lsp="{item, value}">
<v-edit-dialog v-if="writeaccess"
large
@open="editItem(item, 'lsp')"
@save="edit = null"
@cancel="edit.value = item.lsp; edit = null"
>
<span>{{ value }}</span>
<template v-slot:input>
<v-text-field v-if="edit"
type="number"
v-model.number="edit.value"
single-line
>
</v-text-field>
</template>
</v-edit-dialog>
<span v-else>{{ value }}</span>
</template>
<template v-slot:item.ts0="{item, value}">
<v-edit-dialog v-if="writeaccess"
large
@open="editItem(item, 'ts0', item.ts0.toISOString())"
@save="edit = null"
@cancel="edit.value = item.ts0; edit = null"
>
<span>{{ value.toISOString ? value.toISOString().slice(0, 16) : "" }}</span>
<template v-slot:input>
<v-text-field v-if="edit"
type="datetime-local"
v-model="edit.value"
single-line
>
</v-text-field>
</template>
</v-edit-dialog>
<span v-else>{{ value.toISOString ? value.toISOString().slice(0, 16) : "" }}</span>
</template>
<template v-slot:item.ts1="{item, value}">
<v-edit-dialog v-if="writeaccess"
large
@open="editItem(item, 'ts1', item.ts1.toISOString())"
@save="edit = null"
@cancel="edit.value = item.ts1; edit = null"
>
<span>{{ value.toISOString ? value.toISOString().slice(0, 16) : "" }}</span>
<template v-slot:input>
<v-text-field v-if="edit"
type="datetime-local"
v-model="edit.value"
single-line
>
</v-text-field>
</template>
</v-edit-dialog>
<span v-else>{{ value.toISOString ? value.toISOString().slice(0, 16) : "" }}</span>
</template>
<template v-slot:item.length="props">
<span style="white-space:nowrap;">{{ Math.round(props.value) }} m</span>
</template>
<template v-slot:item.azimuth="props">
<span style="white-space:nowrap;">{{ props.value.toFixed(2) }} °</span>
</template>
<template v-slot:item.remarks="{item}">
<v-text-field v-if="writeaccess && edit && edit.sequence == item.sequence && edit.key == 'remarks'"
type="text"
v-model="edit.value"
prepend-icon="mdi-restore"
append-outer-icon="mdi-content-save-edit-outline"
clearable
@click:prepend="edit.value = item.remarks; edit = null"
@click:append-outer="edit = null"
>
</v-text-field>
<div v-else>
<span v-html="$options.filters.markdownInline(item.remarks)"></span>
<v-btn v-if="edit === null && writeaccess"
icon
small
title="Edit"
:disabled="plannedSequencesLoading"
@click="editItem(item, 'remarks')"
>
<v-icon small>mdi-square-edit-outline</v-icon>
</v-btn>
</div>
</template>
<template v-slot:item.speed="{item}">
<v-edit-dialog v-if="writeaccess"
large
@open="editItem(item, 'speed', knots(item).toFixed(1))"
@save="edit = null"
@cancel="edit.value = undefined; edit = null"
>
<span style="white-space:nowrap;">{{ knots(item).toFixed(1) }} kt</span>
<template v-slot:input>
<v-text-field v-if="edit"
type="number"
min="0"
step="0.1"
v-model.number="edit.value"
single-line
>
</v-text-field>
</template>
</v-edit-dialog>
<span v-else style="white-space:nowrap;">{{ knots(item).toFixed(1) }} kt</span>
</template>
<template v-slot:item.lag="{item}">
<v-edit-dialog v-if="writeaccess"
large
@open="editItem(item, 'lagAfter', Math.round(lagAfter(item)/(60*1000)))"
@save="edit = null"
@cancel="edit.value = undefined; edit = null"
>
<span>{{ Math.round(lagAfter(item) / (60*1000)) }} min</span>
<template v-slot:input>
<v-text-field v-if="edit"
type="number"
min="0"
v-model="edit.value"
single-line
>
</v-text-field>
</template>
</v-edit-dialog>
<span v-else>{{ Math.round(lagAfter(item) / (60*1000)) }} min</span>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
</template>
<style lang="stylus" scoped>
</style>
<script>
import suncalc from 'suncalc';
import { mapActions, mapGetters } from 'vuex';
export default {
name: "Plan",
components: {
},
data () {
return {
headers: [
{
value: "sequence",
text: "Sequence"
},
{
value: "srss",
text: "SR/SS"
},
{
value: "name",
text: "Name"
},
{
value: "line",
text: "Line"
},
{
value: "fsp",
text: "FSP",
align: "end"
},
{
value: "lsp",
text: "LSP",
align: "end"
},
{
value: "ts0",
text: "Start"
},
{
value: "ts1",
text: "End"
},
{
value: "num_points",
text: "Num. points",
align: "end"
},
{
value: "length",
text: "Length",
align: "end"
},
{
value: "azimuth",
text: "Azimuth",
align: "end"
},
{
value: "remarks",
text: "Remarks"
},
{
value: "speed",
text: "Speed"
},
{
text: "Line change after",
value: "lag",
sortable: false
}
],
items: [],
remarks: null,
editRemarks: false,
filter: null,
options: {},
sequenceCount: null,
activeItem: null,
edit: null, // {sequence, key, value}
queuedReload: false,
itemsPerPage: 25,
plannerConfig: null,
shiftAll: false, // Shift all sequences checkbox
// Weather API
wxData: null,
weathercode: {
0: {
description: "Clear sky",
icon: "mdi-weather-sunny"
},
1: {
description: "Mainly clear",
icon: "mdi-weather-sunny"
},
2: {
description: "Partly cloudy",
icon: "mdi-weather-partly-cloudy"
},
3: {
description: "Overcast",
icon: "mdi-weather-cloudy"
},
45: {
description: "Fog",
icon: "mde-weather-fog"
},
48: {
description: "Depositing rime fog",
icon: "mdi-weather-fog"
},
51: {
description: "Light drizzle",
icon: "mdi-weather-partly-rainy"
},
53: {
description: "Moderate drizzle",
icon: "mdi-weather-partly-rainy"
},
55: {
description: "Dense drizzle",
icon: "mdi-weather-rainy"
},
56: {
description: "Light freezing drizzle",
icon: "mdi-weather-partly-snowy-rainy"
},
57: {
description: "Freezing drizzle",
icon: "mdi-weather-partly-snowy-rainy"
},
61: {
description: "Light rain",
icon: "mdi-weather-rainy"
},
63: {
description: "Moderate rain",
icon: "mdi-weather-rainy"
},
65: {
description: "Heavy rain",
icon: "mdi-weather-pouring"
},
66: {
description: "Light freezing rain",
icon: "mdi-loading"
},
67: {
description: "Freezing rain",
icon: "mdi-loading"
},
71: {
description: "Light snow",
icon: "mdi-loading"
},
73: {
description: "Moderate snow",
icon: "mdi-loading"
},
75: {
description: "Heavy snow",
icon: "mdi-loading"
},
77: {
description: "Snow grains",
icon: "mdi-loading"
},
80: {
description: "Light rain showers",
icon: "mdi-loading"
},
81: {
description: "Moderate rain showers",
icon: "mdi-loading"
},
82: {
description: "Violent rain showers",
icon: "mdi-loading"
},
85: {
description: "Light snow showers",
icon: "mdi-loading"
},
86: {
description: "Snow showers",
icon: "mdi-loading"
},
95: {
description: "Thunderstorm",
icon: "mdi-loading"
},
96: {
description: "Hailstorm",
icon: "mdi-loading"
},
99: {
description: "Heavy hailstorm",
icon: "mdi-loading"
},
},
// Context menu stuff
contextMenuShow: false,
contextMenuX: 0,
contextMenuY: 0,
contextMenuItem: null
}
},
computed: {
...mapGetters(['user', 'writeaccess', 'plannedSequencesLoading', 'plannedSequences', 'planRemarks'])
},
watch: {
options: {
handler () {
this.fetchPlannedSequences();
},
deep: true
},
async plannedSequences () {
await this.fetchPlannedSequences();
},
async edit (newVal, oldVal) {
if (newVal === null && oldVal !== null) {
const item = this.items.find(i => i.sequence == oldVal.sequence);
// Get around this Vuetify feature
// https://github.com/vuetifyjs/vuetify/issues/4144
if (oldVal.value === null) oldVal.value = "";
if (item) {
if (item[oldVal.key] != oldVal.value) {
if (oldVal.key == "lagAfter") {
// Convert from minutes to seconds
oldVal.value *= 60;
} else if (oldVal.key == "speed") {
// Convert knots to metres per second
oldVal.value = oldVal.value*(1.852/3.6);
}
if (await this.saveItem(oldVal)) {
item[oldVal.key] = oldVal.value;
} else {
this.edit = oldVal;
}
}
}
}
},
filter (newVal, oldVal) {
if (newVal?.toLowerCase() != oldVal?.toLowerCase()) {
this.fetchPlannedSequences();
}
},
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;
}
},
methods: {
suntimes (line) {
const oneday = 86400000;
function isDay (srss, ts, lat, lng) {
if (isNaN(srss.sunriseEnd) || isNaN(srss.sunsetStart)) {
// Between March and September
ts = new Date(ts);
if (ts.getMonth() >= 2 && ts.getMonth() <= 8) {
// Polar day in the Northern hemisphere, night in the South
return lat > 0;
} else {
return lat < 0;
}
} else {
if (srss.sunriseEnd < ts) {
if (ts < srss.sunsetStart) {
return true;
} else {
return suncalc.getTimes(new Date(ts.valueOf() + oneday), lat, lng).sunriseEnd < ts;
}
} else {
return ts < suncalc.getTimes(new Date(ts.valueOf() - oneday), lat, lng).sunsetStart;
}
}
}
let {ts0, ts1} = line;
const [ lng0, lat0 ] = line.geometry.coordinates[0];
const [ lng1, lat1 ] = line.geometry.coordinates[1];
if (ts1-ts0 > oneday) {
console.warn("Cannot provide reliable sunrise / sunset times for lines over 24 hr in this version");
//return null;
}
const srss0 = suncalc.getTimes(ts0, lat0, lng0);
const srss1 = suncalc.getTimes(ts1, lat1, lng1);
srss0.prevDay = suncalc.getTimes(new Date(ts0.valueOf()-oneday), lat0, lng0);
srss1.nextDay = suncalc.getTimes(new Date(ts1.valueOf()+oneday), lat1, lng1);
srss0.isDay = isDay(srss0, ts0, lat0, lng0);
srss1.isDay = isDay(srss1, ts1, lat1, lng1);
return {
ts0: srss0,
ts1: srss1
};
},
srssIcon (line) {
const srss = this.suntimes(line);
const moon = suncalc.getMoonIllumination(line.ts0);
return srss.ts0.isDay && srss.ts1.isDay
? 'mdi-weather-sunny'
: !srss.ts0.isDay && !srss.ts1.isDay
? moon.phase < 0.05
? 'mdi-moon-new'
: moon.phase < 0.25
? 'mdi-moon-waxing-crescent'
: moon.phase < 0.45
? 'mdi-moon-waxing-gibbous'
: moon.phase < 0.55
? 'mdi-moon-full'
: moon.phase < 0.75
? 'mdi-moon-waning-gibbous'
: 'mdi-moon-waning-crescent'
: 'mdi-theme-light-dark';
},
srssMoonPhase (line) {
const ts = new Date((Number(line.ts0)+Number(line.ts1))/2);
const moon = suncalc.getMoonIllumination(ts);
return moon.phase < 0.05
? 'New moon'
: moon.phase < 0.25
? 'Waxing crescent moon'
: moon.phase < 0.45
? 'Waxing gibbous moon'
: moon.phase < 0.55
? 'Full moon'
: moon.phase < 0.75
? 'Waning gibbous moon'
: 'Waning crescent moon';
},
srssInfo (line) {
const srss = this.suntimes(line);
const text = [];
try {
text.push(`Sunset at\t${srss.ts0.prevDay.sunset.toISOString().substr(0, 16)}Z (FSP)`);
text.push(`Sunrise at\t${srss.ts0.sunrise.toISOString().substr(0, 16)}Z (FSP)`);
text.push(`Sunset at\t${srss.ts0.sunset.toISOString().substr(0, 16)}Z (FSP)`);
if (line.ts0.getUTCDate() != line.ts1.getUTCDate()) {
text.push(`Sunrise at\t${srss.ts1.sunrise.toISOString().substr(0, 16)}Z (LSP)`);
text.push(`Sunset at\t${srss.ts1.sunset.toISOString().substr(0, 16)}Z (LSP)`);
}
text.push(`Sunrise at\t${srss.ts1.nextDay.sunrise.toISOString().substr(0, 16)}Z (LSP)`);
} catch (err) {
if (err instanceof RangeError) {
text.push(srss.ts0.isDay ? "Polar day" : "Polar night");
} else {
console.log("ERROR", err);
}
}
if (!srss.ts0.isDay || !srss.ts1.isDay) {
text.push(this.srssMoonPhase(line));
}
return text.join("\n");
},
wxInfo (line) {
function atm(key) {
return line.meta?.wx?.atmospheric?.hourly[key];
}
function mar(key) {
return line.meta?.wx?.marine?.hourly[key];
}
const code = atm("weathercode");
const description = this.weathercode[code]?.description ?? `WMO code ${code}`;
const wind_speed = Math.round(atm("windspeed_10m"));
const wind_direction = String(Math.round(atm("winddirection_10m"))).padStart(3, "0");
const pressure = Math.round(atm("surface_pressure"));
const temperature = Math.round(atm("temperature_2m"));
const humidity = atm("relativehumidity_2m");
const precipitation = atm("precipitation");
const precipitation_probability = atm("precipitation_probability");
const precipitation_str = precipitation_probability
? `\nPrecipitation ${precipitation} mm (prob. ${precipitation_probability}%)`
: ""
const wave_height = mar("wave_height").toFixed(1);
const wave_direction = mar("wave_direction");
const wave_period = mar("wave_period");
return `${description}\n${temperature}° C\n${pressure} hPa\nWind ${wind_speed} kt ${wind_direction}°\nRelative humidity ${humidity}%${precipitation_str}\nWaves ${wave_height} m ${wave_direction}° @ ${wave_period} s`;
},
wxIcon (line) {
const code = line.meta?.wx?.atmospheric?.hourly?.weathercode;
return this.weathercode[code]?.icon ?? "mdi-help";
},
async wxQuery (line) {
function midpoint(line) {
// WARNING Fails if across the antimeridian
const longitude = (line.geometry.coordinates[0][0] + line.geometry.coordinates[1][0])/2;
const latitude = (line.geometry.coordinates[0][1] + line.geometry.coordinates[1][1])/2;
return [ longitude, latitude ];
}
function extract (fcst) {
const τ = (line.ts0.valueOf() + line.ts1.valueOf()) / 2000;
const [idx, ε] = fcst?.hourly?.time?.reduce( (acc, cur, idx) => {
const δ = Math.abs(cur - τ);
const retval = acc
? acc[1] < δ
? acc
: [ idx, δ ]
: [ idx, δ ];
return retval;
});
if (idx) {
const hourly = {};
for (let key in fcst?.hourly) {
fcst.hourly[key] = fcst.hourly[key][idx];
}
}
return fcst;
}
async function fetch_atmospheric (opts) {
const { longitude, latitude, dt0, dt1 } = opts;
const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&hourly=temperature_2m,relativehumidity_2m,precipitation_probability,precipitation,weathercode,pressure_msl,surface_pressure,windspeed_10m,winddirection_10m&daily=uv_index_max&windspeed_unit=kn&timeformat=unixtime&timezone=GMT&start_date=${dt0}&end_date=${dt1}&format=json`;
const init = {};
const res = await fetch (url, init);
if (res?.ok) {
const data = await res.json();
return extract(data);
}
}
async function fetch_marine (opts) {
const { longitude, latitude, dt0, dt1 } = opts;
const url = `https://marine-api.open-meteo.com/v1/marine?latitude=${latitude}&longitude=${longitude}&hourly=wave_height,wave_direction,wave_period&timeformat=unixtime&timezone=GMT&start_date=${dt0}&end_date=${dt1}&format=json`;
const init = {};
const res = await fetch (url, init);
if (res?.ok) {
const data = await res.json();
return extract(data);
}
}
if (line) {
const [ longitude, latitude ] = midpoint(line);
const dt0 = line.ts0.toISOString().substr(0, 10);
const dt1 = line.ts1.toISOString().substr(0, 10);
return {
atmospheric: await fetch_atmospheric({longitude, latitude, dt0, dt1}),
marine: await fetch_marine({longitude, latitude, dt0, dt1})
};
}
},
lagAfter (item) {
const pos = this.items.indexOf(item)+1;
if (pos != 0) {
if (pos < this.items.length) {
const nextItem = this.items[pos];
return nextItem.ts0 - item.ts1;
}
} else {
console.warn("Item not found in list", item);
}
return this.plannerConfig.defaultLineChangeDuration * 60*1000;
},
knots (item) {
const v = item.length / ((item.ts1-item.ts0)/1000); // m/s
return v*3.6/1.852;
},
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 deletePlannedSequence () {
console.log("Delete sequence", this.contextMenuItem.sequence);
const url = `/project/${this.$route.params.project}/plan/${this.contextMenuItem.sequence}`;
const init = {method: "DELETE"};
await this.api([url, init]);
},
editItem (item, key, value) {
this.edit = {
sequence: item.sequence,
key,
value: value === undefined ? item[key] : value
}
},
async saveItem (edit) {
if (!edit) return;
try {
const url = `/project/${this.$route.params.project}/plan/${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;
}
},
async saveRemarks () {
const url = `/project/${this.$route.params.project}/info/plan/remarks`;
let res;
if (this.remarks) {
const init = {
method: "PUT",
headers: { "Content-Type": "text/plain" },
body: this.remarks
};
await this.api([url, init, (e, r) => res = r]);
} else {
const init = {
method: "DELETE"
};
await this.api([url, init, (e, r) => res = r]);
}
if (res && res.ok) {
this.editRemarks = false;
}
},
async getPlannerConfig () {
const url = `/project/${this.$route.params.project}/configuration/planner`;
this.plannerConfig = await this.api([url]) || {
"overlapAfter": 0,
"overlapBefore": 0,
"defaultAcquisitionSpeed": 5,
"defaultLineChangeDuration": 36
}
},
async fetchPlannedSequences (opts = {}) {
const options = {
text: this.filter,
...this.options
};
const res = await this.getPlannedSequences([this.$route.params.project, options]);
this.items = res.sequences;
this.sequenceCount = res.count;
this.remarks = this.planRemarks;
},
setActiveItem (item) {
this.activeItem = this.activeItem == item
? null
: item;
},
...mapActions(["api", "showSnack", "getPlannedSequences"])
},
async mounted () {
await this.getPlannerConfig();
await this.fetchPlannedSequences();
}
}
</script>