Refactor preplots configuration GUI.

This introduces a number of changes, most notably an easier way
to specify fixed width formats and support for configuring
multiple import options (actual SPSv1, SPSv2.1, P1/90, CSV, …)

Note that only the configuration GUI is done, support for actually
importing those formats has not been implemented as of this commit.
This commit is contained in:
D. Berge
2023-11-13 22:18:03 +01:00
parent afe04f5693
commit 8895a948cf
3 changed files with 640 additions and 88 deletions

View File

@@ -143,7 +143,7 @@ export default {
props: {
text: String,
fields: Object,
//fields: Object,
//delimiter: String,
headerRow: { type: [ Boolean, Number ], default: false},
numberedLines: [ Boolean, Number ],
@@ -215,12 +215,18 @@ export default {
// Properties of the sailline object
const keys = [ "incr", "ntba", "remarks", "source_line", "meta.colour" ];
function to_bool (v, missing=false) {
return (v === undefined || v === null)
? missing // Missing value meaning
: /^t(rue)|^[1-9-]+$/i.test(String(v).trim())
}
// To transform the input text into the required format for each field
const transformer = (key) => {
const transformers = {
incr: (v) => Boolean(Number(v)),
ntba: (v) => Boolean(Number(v)),
remarks: String,
incr: (v) => to_bool(v, true),
ntba: (v) => to_bool(v, false),
remarks: (v) => (v === undefined || v === null) ? "" : String,
source_line: Number,
};
return transformers[key] ?? String;

View File

@@ -0,0 +1,520 @@
<template>
<v-card flat>
<v-card-text>
<v-form>
<v-text-field
label="Path"
v-model="value.path"
>
<dougal-file-browser-dialog
slot="append"
v-model="value.path"
:root="rootPath"
></dougal-file-browser-dialog>
</v-text-field>
<v-select
label="Preplot class"
:items="fileClasses"
v-model="fileClass"
></v-select>
<v-select
label="File format"
:items="preplotFileTypes"
v-model="fileType"
></v-select>
<v-text-field v-if="value.class == 'S'"
class="mb-3"
label="Sailline offset"
prefix="±"
type="number"
hint="The value to add/substract to source lines to get to the corresponding sailline"
v-model.number="value.saillineOffset"
>
</v-text-field>
<v-expansion-panels v-if="isFixedWidthFormat"
:value="head.length ? 0 : null"
>
<v-expansion-panel>
<v-expansion-panel-header>Column settings</v-expansion-panel-header>
<v-expansion-panel-content>
<div class="mb-3" style="max-width:fit-content;">
<v-select v-if="head"
label="Sample text size"
hint="Choose how much of the file to display"
:items="sampleSizeItems"
v-model="sampleSize"
></v-select>
</div>
<v-text-field
label="Skip lines"
hint="This lets you to skip file headers if present"
type="number"
min="0"
v-model.number="firstRow"
></v-text-field>
<dougal-fixed-string-decoder
:text="rows"
:fields="fields"
:multiline="true"
:numbered-lines="firstRow"
max-height="300px"
:editable-field-list="false"
></dougal-fixed-string-decoder>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-expansion-panels v-else-if="isDelimitedFormat"
:value="head.length ? 0 : null"
>
<v-expansion-panel>
<v-expansion-panel-header>Column settings</v-expansion-panel-header>
<v-expansion-panel-content>
<v-container>
<v-row>
<v-col cols="6">
<div class="mb-3" style="max-width:fit-content;">
<v-select v-if="head"
label="Sample text size"
hint="Choose how much of the file to display"
:items="sampleSizeItems"
v-model="sampleSize"
></v-select>
</div>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<dougal-delimited-string-decoder
title="Fields"
:text="rows"
:fields.sync="fields"
:header-row.sync="headerRow"
:numbered-lines.sync="firstRow"
:editable-field-list="false"
:delimiter.sync="delimiter"
></dougal-delimited-string-decoder>
</v-col>
</v-row>
</v-container>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-expansion-panels v-else-if="fileClass == 'saillines'"
:value="head.length ? 0 : null"
>
<v-expansion-panel>
<v-expansion-panel-header>Column settings</v-expansion-panel-header>
<v-expansion-panel-content>
<v-container>
<v-row>
<v-col cols="6">
<div class="mb-3" style="max-width:fit-content;">
<v-select v-if="head"
label="Sample text size"
hint="Choose how much of the file to display"
:items="sampleSizeItems"
v-model="sampleSize"
></v-select>
</div>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<dougal-saillines-string-decoder
subtitle="Sailline data"
:text="head"
></dougal-saillines-string-decoder>
</v-col>
</v-row>
</v-container>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-form>
</v-card-text>
</v-card>
</template>
<script>
import DougalFileBrowserDialog from '@/components/file-browser/file-browser-dialog'
import DougalFixedStringDecoder from '@/components/decoder/fixed-string-decoder'
import DougalDelimitedStringDecoder from '@/components/decoder/delimited-string-decoder';
import DougalSaillinesStringDecoder from '@/components/decoder/saillines-string-decoder';
import { mapActions, mapGetters } from 'vuex'
export default {
name: "DougalProjectSettingsPreplotsPreplot",
components: {
DougalFileBrowserDialog,
DougalFixedStringDecoder,
DougalDelimitedStringDecoder,
DougalSaillinesStringDecoder
},
props: {
value: Object,
rootPath: String
},
data () {
return {
fileClasses: [
{ text: "Source points", value: "S" },
{ text: "Vessel points", value: "V" },
{ text: "Sail lines", value: "saillines" }
],
preplotFileTypeList: {
"S": [
{ header: "Fixed width" },
{ text: "SPS v1", value: "sps1", fixedWidth: true },
{ text: "SPS v2.1", value: "sps2.1", fixedWidth: true },
{ text: "P1/90", value: "p190", fixedWidth: true },
{ text: "Other fixed width", value: "fixed-width", fixedWidth: true },
{ header: "Delimited values" },
{ text: "P1/11", value: "p111", delimited: true },
{ text: "CSV", value: "csv", delimited: true },
],
"V": [
{ header: "Fixed width" },
{ text: "SPS v1", value: "sps1", fixedWidth: true },
{ text: "SPS v2.1", value: "sps2.1", fixedWidth: true },
{ text: "P1/90", value: "p190", fixedWidth: true },
{ text: "Other fixed width", value: "fixed-width", fixedWidth: true },
{ header: "Delimited values" },
{ text: "P1/11", value: "p111", delimited: true },
{ text: "CSV", value: "csv", delimited: true },
],
"saillines": [
{ text: "Sail lines CSV", value: "x-sl+csv" }
]
},
head: "",
sampleSize: 8192, //1024;
sampleSizeItems: [
{ text: "512 bytes", value: 512 },
{ text: "1 kiB", value: 1024 },
{ text: "2 kiB", value: 1024*2 },
{ text: "4 kiB", value: 1024*4 },
{ text: "8 kiB", value: 1024*8 },
{ text: "16 kiB", value: 1024*16 },
{ text: "32 kiB", value: 1024*32 },
],
};
},
computed: {
preplotFileTypes () {
return this.preplotFileTypeList[this.fileClass] ?? [];
},
isFixedWidthFormat () {
return this.preplotFileTypes.find(i => i.value == this.fileType)?.fixedWidth;
},
isDelimitedFormat () {
return this.preplotFileTypes.find(i => i.value == this.fileType)?.delimited;
},
rows () {
if (this.head) {
if (this.firstRow) {
return this.head.split("\n").slice(this.firstRow).join("\n");
}
return this.head;
}
return "";
},
fields: {
get () {
return (this.value?.fields && Object.keys(this.value.fields).length)
? this.value.fields
: {
line_name: {
type: "int"
},
point_number: {
type: "int"
},
easting: {
type: "float"
},
northing: {
type: "float"
}
};
},
set (v) {
// console.log("set fields", v);
this.$emit("input", {
...this.value,
fields: {...v}
});
}
},
fileClass: {
get () {
return this.value?.class;
},
set (v) {
this.$emit("input", {
...this.value,
class: v
});
}
},
fileType: {
get () {
return this.value?.type;
},
set (v) {
this.$emit("input", {
...this.value,
type: v
});
}
},
firstRow: {
get () {
return this.value?.firstRow ?? 0;
},
set (v) {
this.$emit("input", {
...this.value,
firstRow: v
});
}
},
headerRow: {
get () {
return this.value?.headerRow ?? false;
},
set (v) {
this.$emit("input", {
...this.value,
headerRow: v
});
}
},
delimiter: {
get () {
return this.value?.delimiter;
},
set (v) {
this.$emit("input", {
...this.value,
delimiter: v
});
}
}
},
watch: {
async "value.path" (cur, prev) {
if (cur != prev) {
this.head = await this.getHead();
}
},
async sampleSize (cur, prev) {
if (cur && cur != prev) {
this.head = await this.getHead();
}
},
fileClass (cur, prev) {
if (cur != prev && cur == "saillines") {
this.fileType = "x-sl+csv"
}
},
fileType (cur, prev) {
switch (cur) {
case prev:
return;
case "sps1":
this.fields = {
line_name: {
offset: 1,
length: 16,
type: "int"
},
point_number: {
offset: 17,
length: 8,
type: "int"
},
easting: {
offset: 46,
length: 9,
type: "float"
},
northing: {
offset: 55,
length: 10,
type: "float"
}
};
break;
case "sps2.1":
this.fields = {
line_name: {
offset: 1,
length: 7,
type: "int" // SPS v2.1 has this as float but Dougal doesn't support that
},
point_number: {
offset: 11,
length: 7,
type: "int" // Ditto
},
easting: {
offset: 46,
length: 9,
type: "float"
},
northing: {
offset: 55,
length: 10,
type: "float"
}
};
break;
case "p190":
this.fields = {
line_name: {
offset: 1,
length: 12,
type: "int"
},
point_number: {
offset: 19,
length: 6,
type: "int"
},
easting: {
offset: 46,
length: 9,
type: "float"
},
northing: {
offset: 55,
length: 9,
type: "float"
}
};
break;
case "fixed-width":
this.fields = {
line_name: {
offset: 1,
length: 4,
type: "int"
},
point_number: {
offset: 11,
length: 4,
type: "int"
},
easting: {
offset: 44,
length: 8,
type: "float"
},
northing: {
offset: 53,
length: 9,
type: "float"
}
};
case "csv":
this.fields = {
line_name: {
column: 0,
type: "int"
},
point_number: {
column: 1,
type: "int"
},
easting: {
column: 2,
type: "float"
},
northing: {
column: 3,
type: "float"
}
};
break
case "x-sl+csv":
this.fields = null;
break;
}
}
},
methods: {
async getHead () {
console.log("getHead", this.value?.path);
if (this.value?.path) {
const url = `/files/${this.value.path}`;
const init = {
text: true,
headers: {
"Range": `bytes=0-${this.sampleSize}`
}
};
const head = await this.api([url, init]);
return head?.substring(0, head.lastIndexOf("\n")) || "";
}
return "";
},
...mapActions(["api"])
},
created () {
this.$nextTick(async () => {
this.head = await this.getHead();
});
}
}
</script>

View File

@@ -3,51 +3,43 @@
<v-card-title>Preplots</v-card-title>
<v-card-subtitle>Preplot files location and format.</v-card-subtitle>
<v-card-text>
<v-tabs v-model="tab">
<v-tab v-for="item in preplots">{{tabNameFor(item)}}</v-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item v-for="item in preplots">
<v-card flat>
<v-card-text>
<v-form>
<v-text-field
label="Path"
v-model="item.path"
>
<dougal-file-browser-dialog
slot="append"
v-model="item.path"
:root="value.rootPath"
:mimetypes="[ 'text/plain', '*.sps' ]"
></dougal-file-browser-dialog>
</v-text-field>
<v-expansion-panels v-model="panel">
<v-expansion-panel v-for="(preplot, idx) in preplots_" :key="idx">
<v-expansion-panel-header>
{{ titleFor(preplot) }}
</v-expansion-panel-header>
<v-expansion-panel-content>
<dougal-project-settings-preplots-preplot
v-model="preplots_[idx]"
:root-path="rootPath"
></dougal-project-settings-preplots-preplot>
<v-text-field v-if="item.class == 'S'"
class="mb-3"
label="Sailline offset"
prefix="
type="number"
hint="The value to add/substract to source lines to get to the corresponding sailline"
v-model.number="item.saillineOffset"
>
</v-text-field>
<v-btn
outlined
color="red"
title="Delete this preplot definition"
@click.stop="deletePreplot(preplot)"
>
<v-icon left>mdi-delete-outline</v-icon>
Delete
</v-btn>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-btn
class="mt-5"
color="primary"
:disabled="!lastPreplotIsValid"
@click="newPreplot"
>
<v-icon left>mdi-file-document-plus-outline</v-icon>
New preplot
</v-btn>
<v-expansion-panels :value="head.length ? 0 : null">
<v-expansion-panel>
<v-expansion-panel-header>Column settings</v-expansion-panel-header>
<v-expansion-panel-content>
<dougal-fixed-width-format v-model="item.format" :sample="head">
</dougal-fixed-width-format>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-form>
</v-card-text>
</v-card>
</v-tab-item>
</v-tabs-items>
</v-card-text>
<v-card-actions>
<v-btn
@@ -77,45 +69,42 @@
</style>
<script>
import DougalFileBrowserDialog from '@/components/file-browser/file-browser-dialog';
import DougalFixedWidthFormat from './fixed-width-format';
import { mapActions, mapGetters } from 'vuex';
import DougalProjectSettingsPreplotsPreplot from '@/components/project-settings/preplots-preplot';
export default {
name: "DougalProjectSettingsPreplots",
components: {
DougalFileBrowserDialog,
DougalFixedWidthFormat
DougalProjectSettingsPreplotsPreplot,
},
props: [ "value" ],
props: [ "rootPath", "preplots" ],
data () {
return {
tab: null,
preplots: [],
head: ""
preplots_: [],
panel: null
}
},
computed: {
currentItem () {
if (this.tab !== null) {
return this.preplots[this.tab];
}
lastPreplotIsValid () {
return this.preplots_.length == 0 || this.validPreplot(this.preplots_[this.preplots_.length-1]);
},
isValid () {
return this.preplots && this.preplots.every( item => item.format != null );
return this.preplots_.every(preplot => this.validPreplot(preplot));
}
},
watch: {
value (newValue) {
this.reset();
preplots () {
if (this.preplots.some( (i, idx) => i != this.preplots_[idx] )) {
this.preplots_ = structuredClone(this.preplots);
}
},
currentItem: {
@@ -130,47 +119,84 @@ export default {
methods: {
tabNameFor (item) {
switch (item.class) {
case "S":
return "Source";
case "V":
return "Vessel";
default:
return item.class || "?";
titleFor (preplot) {
let str = "";
if (preplot?.path) {
str += preplot.path;
} else {
const idx = this.preplots_.findIndex(i => i == preplot);
if (idx != -1) {
str += `Preplot ${idx}`;
} else {
str += "Preplot <no path>";
}
}
if (preplot?.class || preplot?.type) {
str += " (" + [preplot.class, preplot.type].join("; ") + ")";
}
return str;
},
validPreplot (preplot) {
const predefined_formats = [ "sps1", "sps2.1", "p190", "p111", "x-sl+csv" ]
const common = (preplot?.path &&
preplot?.type &&
preplot?.class &&
preplot?.fields);
if (common) {
if (predefined_formats.includes(preplot.class)) {
// Predefined formats do not require a field definition
return true;
} else {
return !!(preplot?.fields?.line_name &&
preplot?.fields?.point_number &&
preplot?.fields?.easting &&
preplot?.fields?.northing);
}
}
return false;
},
deletePreplot (preplot) {
const idx = this.preplots_.find(i => i == preplot);
if (idx != -1) {
this.preplots_.splice(idx, 1);
}
},
async getHead () {
if (this.currentItem) {
const url = `/files/${this.currentItem.path}`;
const init = {
text: true,
headers: {
"Range": "bytes=0-1024"
}
newPreplot () {
if (this.lastPreplotIsValid) {
const preplot = {
path: "",
type: "",
format: "",
fields: {},
};
this.head = await this.api([url, init]);
this.head = this.head?.substring(0, this.head.lastIndexOf("\n")) || "";
this.preplots_.push(preplot);
this.panel = this.preplots_.length - 1;
}
},
reset () {
if (!this.value) {
return;
}
this.preplots = JSON.parse(JSON.stringify(this.value.preplots));
this.preplots_ = this.preplots;
},
save () {
this.$emit('input', {preplots: [...this.preplots]});
this.$emit('input', {
preplots: this.preplots_
});
},
back () {
this.$emit('close');
},
...mapActions(["api"])
}
},