mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:17:07 +00:00
Compare commits
231 Commits
287-the-pr
...
v2025.30.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5af6df052 | ||
|
|
0c5ea7f30a | ||
|
|
302642f88d | ||
|
|
48e1369088 | ||
|
|
daa700e7dc | ||
|
|
8db2c8ce25 | ||
|
|
890e48e078 | ||
|
|
11829555cf | ||
|
|
07d8e97f74 | ||
|
|
fc379aba14 | ||
|
|
8cbacb9aa7 | ||
|
|
acb59035e4 | ||
|
|
b7d0ee7da7 | ||
|
|
3a0f720f2f | ||
|
|
6cf6fe29f4 | ||
|
|
6f0f2dadcc | ||
|
|
64fba1adc3 | ||
|
|
3ea82cb660 | ||
|
|
84c1385f88 | ||
|
|
b1b7332216 | ||
|
|
8e7451e17a | ||
|
|
bdeb2b8742 | ||
|
|
ccfabf84f7 | ||
|
|
5d4e219403 | ||
|
|
3b7e4c9f0b | ||
|
|
683f5680b1 | ||
|
|
ce901a03a1 | ||
|
|
f8e5b74c1a | ||
|
|
ec41d26a7a | ||
|
|
386fd59900 | ||
|
|
e47020a21e | ||
|
|
b8f58ac67c | ||
|
|
b3e27ed1b9 | ||
|
|
f5441d186f | ||
|
|
d58bc4d62e | ||
|
|
01d1691def | ||
|
|
bc444fc066 | ||
|
|
989ec84852 | ||
|
|
065f6617af | ||
|
|
825530c1fe | ||
|
|
1ef8eb871f | ||
|
|
2e9c603ab8 | ||
|
|
7f067ff760 | ||
|
|
487c297747 | ||
|
|
cfa771a830 | ||
|
|
3905e6f5d8 | ||
|
|
2657c42dcc | ||
|
|
63e6af545a | ||
|
|
d6fb7404b1 | ||
|
|
8188766a81 | ||
|
|
b7ae657137 | ||
|
|
1295ec2ee3 | ||
|
|
7c6d3fe5ee | ||
|
|
15570e0f3d | ||
|
|
d551e67042 | ||
|
|
6b216f7406 | ||
|
|
a7e02c526b | ||
|
|
55855d66e9 | ||
|
|
ae79d90fef | ||
|
|
c8b2047483 | ||
|
|
d21cde20fc | ||
|
|
10580ea3ec | ||
|
|
25f83d1eb3 | ||
|
|
dc294b5b50 | ||
|
|
b035d3481c | ||
|
|
ca4a14ffd9 | ||
|
|
d77f7f66db | ||
|
|
6b6f545b9f | ||
|
|
bdf62e2d8b | ||
|
|
1895168889 | ||
|
|
8c875ea2f9 | ||
|
|
addbe2d572 | ||
|
|
85f092b9e1 | ||
|
|
eb99d74e4a | ||
|
|
e65afdcaa1 | ||
|
|
0b7e9e1d01 | ||
|
|
9ad17de4cb | ||
|
|
071fd7438b | ||
|
|
9cc21ba06a | ||
|
|
712b20c596 | ||
|
|
8bbe3aee70 | ||
|
|
dc22bb95fd | ||
|
|
0ef2e60d15 | ||
|
|
289d50d9c1 | ||
|
|
3189a06d75 | ||
|
|
9ef551db76 | ||
|
|
e6669026fa | ||
|
|
12082b91a3 | ||
|
|
7db9155899 | ||
|
|
f8692afad3 | ||
|
|
028cab5188 | ||
|
|
fc73fbfb9f | ||
|
|
96a8d3689a | ||
|
|
7a7106e735 | ||
|
|
d5a10ca273 | ||
|
|
e398f2d3cd | ||
|
|
d154e75797 | ||
|
|
af0df23cc4 | ||
|
|
ec26285e53 | ||
|
|
83b3ec5103 | ||
|
|
86aaade428 | ||
|
|
fbb4e1efaf | ||
|
|
73fb7a5053 | ||
|
|
bc5dfe9c2a | ||
|
|
524420d945 | ||
|
|
e48c734ea9 | ||
|
|
5aaad01238 | ||
|
|
90782c1b09 | ||
|
|
4368cb8571 | ||
|
|
40bc1f9293 | ||
|
|
8c6eefed97 | ||
|
|
59971a43fe | ||
|
|
a2a5a783a3 | ||
|
|
d3bdeff8df | ||
|
|
4a2bed257d | ||
|
|
995e0b9f81 | ||
|
|
3488c8bf4d | ||
|
|
7e1023f6e8 | ||
|
|
41e058ac64 | ||
|
|
2086133109 | ||
|
|
bb70cf1a3d | ||
|
|
be0d7b269f | ||
|
|
934b921f69 | ||
|
|
c20b3b64c7 | ||
|
|
8ec918bc7c | ||
|
|
6fa0f8e659 | ||
|
|
a9f93cfd17 | ||
|
|
9785f4541b | ||
|
|
62ab06b4a7 | ||
|
|
c7270febfc | ||
|
|
2dffd93cfe | ||
|
|
867a534910 | ||
|
|
60aaaf9e04 | ||
|
|
b64a99ab19 | ||
|
|
69fce0e0dc | ||
|
|
8dd971ffec | ||
|
|
fd84eb1ebb | ||
|
|
53b4213a05 | ||
|
|
3fbc266809 | ||
|
|
66a758d91f | ||
|
|
6cebf376d0 | ||
|
|
02adbdf530 | ||
|
|
2357381ee6 | ||
|
|
5245e6a135 | ||
|
|
d93b8f8a9c | ||
|
|
8b47fc4753 | ||
|
|
a0b3568a10 | ||
|
|
8895a948cf | ||
|
|
afe04f5693 | ||
|
|
c3a56bf7e2 | ||
|
|
18fcf42bc3 | ||
|
|
ad48ac9998 | ||
|
|
7ab6be5c67 | ||
|
|
2f56d377c5 | ||
|
|
d1c041995d | ||
|
|
399e86be87 | ||
|
|
13f68d7314 | ||
|
|
80de0c1bb0 | ||
|
|
26a487aa47 | ||
|
|
53e7a06a18 | ||
|
|
efe64f0a8c | ||
|
|
313e9687bd | ||
|
|
09fb653812 | ||
|
|
0137bd84d5 | ||
|
|
f82f2c78c7 | ||
|
|
9f1fc3d19c | ||
|
|
873d7cfea7 | ||
|
|
2fa9d99eeb | ||
|
|
12b28cbb8d | ||
|
|
436a9b8289 | ||
|
|
b3dbc0f417 | ||
|
|
6d417a9272 | ||
|
|
b74419f770 | ||
|
|
cae57e2a64 | ||
|
|
cd739e603f | ||
|
|
beeba966dd | ||
|
|
544c4ead76 | ||
|
|
4486fc4afc | ||
|
|
a55d2cc6fc | ||
|
|
402a3f9cce | ||
|
|
1801fdb052 | ||
|
|
be904d8a00 | ||
|
|
2131cdf0c1 | ||
|
|
15242de2d9 | ||
|
|
b4aed52976 | ||
|
|
1b85b5cd4b | ||
|
|
f157f49312 | ||
|
|
3d42ce6fbc | ||
|
|
4595dddc24 | ||
|
|
642f5a7585 | ||
|
|
e7c29ba14c | ||
|
|
d919fb12db | ||
|
|
c21f9c239e | ||
|
|
2fb1c5fdcc | ||
|
|
c6b99563d9 | ||
|
|
76a90df768 | ||
|
|
ea8ea12429 | ||
|
|
7bd2319cd7 | ||
|
|
a9270157ea | ||
|
|
d2f94dbb88 | ||
|
|
1056122fff | ||
|
|
9bd0aca18f | ||
|
|
60932300c1 | ||
|
|
12307b7ae6 | ||
|
|
ceeaa4a8f3 | ||
|
|
3da54f9334 | ||
|
|
4c612ffe0a | ||
|
|
7076b51a25 | ||
|
|
fe5ca06060 | ||
|
|
71467dddf9 | ||
|
|
246f01efbe | ||
|
|
68bf853594 | ||
|
|
4a18cb8a81 | ||
|
|
c615727acf | ||
|
|
2e21526fca | ||
|
|
3709070985 | ||
|
|
2ac963aa4f | ||
|
|
db7b385d66 | ||
|
|
d91a1b1302 | ||
|
|
fa031d5fc9 | ||
|
|
620d5ccf47 | ||
|
|
f0fa2b75d5 | ||
|
|
46bb207dfb | ||
|
|
f7a386d179 | ||
|
|
e4607a095b | ||
|
|
4b0d42390f | ||
|
|
114e41557f | ||
|
|
e605320503 | ||
|
|
6606c7a6c1 | ||
|
|
e3bf671a49 | ||
|
|
3e08dfd45b |
@@ -256,6 +256,78 @@ class Datastore:
|
||||
|
||||
self.maybe_commit()
|
||||
|
||||
|
||||
def save_preplot_line_info(self, lines, filepath, filedata = None):
|
||||
"""
|
||||
Save preplot line information
|
||||
|
||||
Arguments:
|
||||
|
||||
lines (iterable): should be a collection of lines returned from
|
||||
one of the line info reading functions (see preplots.py).
|
||||
|
||||
filepath (string): the full path to the preplot file from where the lines
|
||||
have been read. It will be added to the survey's `file` table so that
|
||||
it can be monitored for changes.
|
||||
"""
|
||||
|
||||
with self.conn.cursor() as cursor:
|
||||
cursor.execute("BEGIN;")
|
||||
|
||||
# Check which preplot lines we actually have already imported,
|
||||
# as the line info file may contain extra lines.
|
||||
|
||||
qry = """
|
||||
SELECT line, class
|
||||
FROM preplot_lines
|
||||
ORDER BY line, class;
|
||||
"""
|
||||
cursor.execute(qry)
|
||||
preplot_lines = cursor.fetchall()
|
||||
|
||||
hash = self.add_file(filepath, cursor)
|
||||
count=0
|
||||
for line in lines:
|
||||
count += 1
|
||||
|
||||
if not (line["sail_line"], "V") in preplot_lines:
|
||||
print(f"\u001b[2KSkipping line {count} / {len(lines)}", end="\n", flush=True)
|
||||
continue
|
||||
|
||||
print(f"\u001b[2KSaving line {count} / {len(lines)} ", end="\n", flush=True)
|
||||
|
||||
sail_line = line["sail_line"]
|
||||
incr = line.get("incr", True)
|
||||
ntba = line.get("ntba", False)
|
||||
remarks = line.get("remarks", None)
|
||||
meta = json.dumps(line.get("meta", {}))
|
||||
source_lines = line.get("source_line", [])
|
||||
|
||||
for source_line in source_lines:
|
||||
qry = """
|
||||
INSERT INTO preplot_saillines AS ps
|
||||
(sailline, line, sailline_class, line_class, incr, ntba, remarks, meta, hash)
|
||||
VALUES
|
||||
(%s, %s, 'V', 'S', %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (sailline, sailline_class, line, line_class, incr) DO UPDATE
|
||||
SET
|
||||
incr = EXCLUDED.incr,
|
||||
ntba = EXCLUDED.ntba,
|
||||
remarks = COALESCE(EXCLUDED.remarks, ps.remarks),
|
||||
meta = ps.meta || EXCLUDED.meta,
|
||||
hash = EXCLUDED.hash;
|
||||
"""
|
||||
|
||||
# NOTE Consider using cursor.executemany() instead. Then again,
|
||||
# we're only expecting a few hundred lines at most.
|
||||
cursor.execute(qry, (sail_line, source_line, incr, ntba, remarks, meta, hash))
|
||||
|
||||
if filedata is not None:
|
||||
self.save_file_data(filepath, json.dumps(filedata), cursor)
|
||||
|
||||
self.maybe_commit()
|
||||
|
||||
|
||||
def save_raw_p190(self, records, fileinfo, filepath, epsg = 0, filedata = None, ntbp = False):
|
||||
"""
|
||||
Save raw P1 data.
|
||||
@@ -627,12 +699,12 @@ class Datastore:
|
||||
|
||||
if include_archived:
|
||||
qry = """
|
||||
SELECT meta
|
||||
SELECT meta, schema
|
||||
FROM public.projects;
|
||||
"""
|
||||
else:
|
||||
qry = """
|
||||
SELECT meta
|
||||
SELECT meta, schema
|
||||
FROM public.projects
|
||||
WHERE NOT (meta->'archived')::boolean IS true
|
||||
"""
|
||||
@@ -642,7 +714,12 @@ class Datastore:
|
||||
|
||||
cursor.execute(qry)
|
||||
results = cursor.fetchall()
|
||||
return [r[0] for r in results if r[0]]
|
||||
surveys = []
|
||||
for r in results:
|
||||
if r[0]:
|
||||
r[0]['schema'] = r[1]
|
||||
surveys.append(r[0])
|
||||
return surveys
|
||||
|
||||
|
||||
# TODO Does this need tweaking on account of #246?
|
||||
|
||||
163
bin/delimited.py
Normal file
163
bin/delimited.py
Normal file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
"""
|
||||
Delimited record importing functions.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import builtins
|
||||
|
||||
def to_bool (v):
|
||||
try:
|
||||
return bool(int(v))
|
||||
except ValueError:
|
||||
if type(v) == str:
|
||||
return v.strip().lower().startswith("t")
|
||||
return False
|
||||
|
||||
transform = {
|
||||
"int": lambda v: builtins.int(float(v)),
|
||||
"float": float,
|
||||
"string": str,
|
||||
"bool": to_bool
|
||||
}
|
||||
|
||||
def cast_values (row, fields):
|
||||
|
||||
def enum_for (key):
|
||||
field = fields.get(key, {})
|
||||
def enum (val):
|
||||
if "enum" in field:
|
||||
ret_val = field.get("default", val)
|
||||
enums = field.get("enum", [])
|
||||
for enum_key in enums:
|
||||
if enum_key == val:
|
||||
ret_val = enums[enum_key]
|
||||
return ret_val
|
||||
return val
|
||||
return enum
|
||||
|
||||
# Get rid of any unwanted data
|
||||
if None in row:
|
||||
del(row[None])
|
||||
|
||||
for key in row:
|
||||
|
||||
val = row[key]
|
||||
enum = enum_for(key)
|
||||
transformer = transform.get(fields.get(key, {}).get("type"), str)
|
||||
|
||||
if type(val) == list:
|
||||
for i, v in enumerate(val):
|
||||
row[key][i] = transformer(enum(v))
|
||||
elif type(val) == dict:
|
||||
continue
|
||||
else:
|
||||
row[key] = transformer(enum(val))
|
||||
return row
|
||||
|
||||
def build_fieldnames (spec): #(arr, key, val):
|
||||
fieldnames = []
|
||||
|
||||
if "fields" in spec:
|
||||
for key in spec["fields"]:
|
||||
index = spec["fields"][key]["column"]
|
||||
try:
|
||||
fieldnames[index] = key
|
||||
except IndexError:
|
||||
assert index >= 0
|
||||
fieldnames.extend(((index + 1) - len(fieldnames)) * [None])
|
||||
fieldnames[index] = key
|
||||
|
||||
return fieldnames
|
||||
|
||||
|
||||
def from_file_delimited (path, spec):
|
||||
|
||||
fieldnames = build_fieldnames(spec)
|
||||
fields = spec.get("fields", [])
|
||||
delimiter = spec.get("delimiter", ",")
|
||||
firstRow = spec.get("firstRow", 0)
|
||||
headerRow = spec.get("headerRow", False)
|
||||
if headerRow:
|
||||
firstRow += 1
|
||||
|
||||
records = []
|
||||
with open(path, "r", errors="ignore") as fd:
|
||||
|
||||
if spec.get("type") == "x-sl+csv":
|
||||
fieldnames = None # Pick from header row
|
||||
firstRow = 0
|
||||
reader = csv.DictReader(fd, delimiter=delimiter)
|
||||
else:
|
||||
reader = csv.DictReader(fd, fieldnames=fieldnames, delimiter=delimiter)
|
||||
|
||||
row = 0
|
||||
for line in reader:
|
||||
skip = False
|
||||
|
||||
if row < firstRow:
|
||||
skip = True
|
||||
|
||||
if not skip:
|
||||
records.append(cast_values(dict(line), fields))
|
||||
|
||||
row += 1
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def remap (line, headers):
|
||||
row = dict()
|
||||
for i, key in enumerate(headers):
|
||||
if "." in key[1:-1]:
|
||||
# This is an object
|
||||
k, attr = key.split(".")
|
||||
if not k in row:
|
||||
row[k] = dict()
|
||||
row[k][attr] = line[i]
|
||||
elif key in row:
|
||||
if type(row[key]) == list:
|
||||
row[key].append(line[i])
|
||||
else:
|
||||
row[key] = [ row[key], line[i] ]
|
||||
else:
|
||||
row[key] = line[i]
|
||||
return row
|
||||
|
||||
def from_file_saillines (path, spec):
|
||||
|
||||
fields = {
|
||||
"sail_line": { "type": "int" },
|
||||
"source_line": { "type": "int" },
|
||||
"incr": { "type": "bool" },
|
||||
"ntba": { "type": "bool" }
|
||||
}
|
||||
|
||||
# fields = spec.get("fields", sl_fields)
|
||||
delimiter = spec.get("delimiter", ",")
|
||||
firstRow = spec.get("firstRow", 0)
|
||||
|
||||
records = []
|
||||
with open(path, "r", errors="ignore") as fd:
|
||||
row = 0
|
||||
reader = csv.reader(fd, delimiter=delimiter)
|
||||
while row < firstRow:
|
||||
next(reader)
|
||||
row += 1
|
||||
headers = [ h.strip() for h in next(reader) if len(h.strip()) ]
|
||||
|
||||
for line in reader:
|
||||
records.append(cast_values(remap(line, headers), fields))
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def from_file_p111 (path, spec):
|
||||
pass
|
||||
|
||||
def from_file (path, spec):
|
||||
if spec.get("type") == "x-sl+csv":
|
||||
return from_file_saillines(path, spec)
|
||||
else:
|
||||
return from_file_delimited(path, spec)
|
||||
126
bin/fwr.py
Normal file
126
bin/fwr.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
"""
|
||||
Fixed width record importing functions.
|
||||
"""
|
||||
|
||||
import builtins
|
||||
|
||||
def to_bool (v):
|
||||
try:
|
||||
return bool(int(v))
|
||||
except ValueError:
|
||||
if type(v) == str:
|
||||
return v.strip().lower().startswith("t")
|
||||
return False
|
||||
|
||||
transform = {
|
||||
"int": lambda v: builtins.int(float(v)),
|
||||
"float": float,
|
||||
"string": str,
|
||||
"str": str,
|
||||
"bool": to_bool
|
||||
}
|
||||
|
||||
def parse_line (line, fields, fixed = None):
|
||||
data = dict()
|
||||
|
||||
if fixed:
|
||||
for value in fixed:
|
||||
start = value["offset"]
|
||||
end = start + len(value["text"])
|
||||
text = line[start:end]
|
||||
if text != value["text"]:
|
||||
return f"Expected text `{value['text']}` at position {start} but found `{text}` instead."
|
||||
|
||||
for key in fields:
|
||||
spec = fields[key]
|
||||
transformer = transform[spec.get("type", "str")]
|
||||
pos_from = spec["offset"]
|
||||
pos_to = pos_from + spec["length"]
|
||||
text = line[pos_from:pos_to]
|
||||
value = transformer(text)
|
||||
if "enum" in spec:
|
||||
if "default" in spec:
|
||||
value = spec["default"]
|
||||
for enum_key in spec["enum"]:
|
||||
if enum_key == text:
|
||||
enum_value = transformer(spec["enum"][enum_key])
|
||||
value = enum_value
|
||||
break
|
||||
|
||||
data[key] = value
|
||||
|
||||
return data
|
||||
|
||||
|
||||
specfields = {
|
||||
"sps1": {
|
||||
"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" }
|
||||
},
|
||||
"sps21": {
|
||||
"line_name": { "offset": 1, "length": 7, "type": "int" },
|
||||
"point_number": { "offset": 11, "length": 7, "type": "int" },
|
||||
"easting": { "offset": 46, "length": 9, "type": "float" },
|
||||
"northing": { "offset": 55, "length": 10, "type": "float" }
|
||||
},
|
||||
"p190": {
|
||||
"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" }
|
||||
},
|
||||
}
|
||||
|
||||
def from_file(path, spec):
|
||||
|
||||
# If spec.fields is not present, deduce it from spec.type ("sps1", "sps21", "p190", etc.)
|
||||
if "fields" in spec:
|
||||
fields = spec["fields"]
|
||||
elif "type" in spec and spec["type"] in specfields:
|
||||
fields = specfields[spec["type"]]
|
||||
else:
|
||||
# TODO: Should default to looking for spec.format and doing a legacy import on it
|
||||
return "Neither 'type' nor 'fields' given. I don't know how to import this fixed-width dataset."
|
||||
|
||||
firstRow = spec.get("firstRow", 0)
|
||||
|
||||
skipStart = [] # Skip lines starting with any of these values
|
||||
skipMatch = [] # Skip lines matching any of these values
|
||||
|
||||
if "type" in spec:
|
||||
if spec["type"] == "sps1" or spec["type"] == "sps21" or spec["type"] == "p190":
|
||||
skipStart = "H"
|
||||
skipMatch = "EOF"
|
||||
|
||||
records = []
|
||||
with open(path, "r", errors="ignore") as fd:
|
||||
row = 0
|
||||
line = fd.readline()
|
||||
|
||||
while line:
|
||||
skip = False
|
||||
|
||||
if row < firstRow:
|
||||
skip = True
|
||||
|
||||
if not skip:
|
||||
for v in skipStart:
|
||||
if line.startswith(v):
|
||||
skip = True
|
||||
break
|
||||
for v in skipMatch:
|
||||
if line == v:
|
||||
skip = True
|
||||
break
|
||||
|
||||
if not skip:
|
||||
records.append(parse_line(line, fields))
|
||||
|
||||
row += 1
|
||||
line = fd.readline()
|
||||
|
||||
return records
|
||||
@@ -15,6 +15,7 @@ import re
|
||||
import time
|
||||
import configuration
|
||||
import p111
|
||||
import fwr
|
||||
from datastore import Datastore
|
||||
|
||||
def add_pending_remark(db, sequence):
|
||||
@@ -69,8 +70,12 @@ if __name__ == '__main__':
|
||||
print("No final P1/11 configuration")
|
||||
exit(0)
|
||||
|
||||
pattern = final_p111["pattern"]
|
||||
rx = re.compile(pattern["regex"])
|
||||
|
||||
lineNameInfo = final_p111.get("lineNameInfo")
|
||||
pattern = final_p111.get("pattern")
|
||||
rx = None
|
||||
if pattern and pattern.get("regex"):
|
||||
rx = re.compile(pattern["regex"])
|
||||
|
||||
if "pending" in survey["final"]:
|
||||
pendingRx = re.compile(survey["final"]["pending"]["pattern"]["regex"])
|
||||
@@ -98,15 +103,38 @@ if __name__ == '__main__':
|
||||
|
||||
print("Importing")
|
||||
|
||||
match = rx.match(os.path.basename(logical_filepath))
|
||||
if not match:
|
||||
error_message = f"File path not match the expected format! ({logical_filepath} ~ {pattern['regex']})"
|
||||
print(error_message, file=sys.stderr)
|
||||
print("This file will be ignored!")
|
||||
continue
|
||||
if rx:
|
||||
match = rx.match(os.path.basename(logical_filepath))
|
||||
if not match:
|
||||
error_message = f"File path not match the expected format! ({logical_filepath} ~ {pattern['regex']})"
|
||||
print(error_message, file=sys.stderr)
|
||||
print("This file will be ignored!")
|
||||
continue
|
||||
|
||||
file_info = dict(zip(pattern["captures"], match.groups()))
|
||||
file_info["meta"] = {}
|
||||
file_info = dict(zip(pattern["captures"], match.groups()))
|
||||
file_info["meta"] = {}
|
||||
|
||||
if lineNameInfo:
|
||||
basename = os.path.basename(physical_filepath)
|
||||
fields = lineNameInfo.get("fields", {})
|
||||
fixed = lineNameInfo.get("fixed")
|
||||
try:
|
||||
parsed_line = fwr.parse_line(basename, fields, fixed)
|
||||
except ValueError as err:
|
||||
parsed_line = "Line format error: " + str(err)
|
||||
if type(parsed_line) == str:
|
||||
print(parsed_line, file=sys.stderr)
|
||||
print("This file will be ignored!")
|
||||
continue
|
||||
|
||||
file_info = {}
|
||||
file_info["sequence"] = parsed_line["sequence"]
|
||||
file_info["line"] = parsed_line["line"]
|
||||
del(parsed_line["sequence"])
|
||||
del(parsed_line["line"])
|
||||
file_info["meta"] = {
|
||||
"fileInfo": parsed_line
|
||||
}
|
||||
|
||||
if pending:
|
||||
print("Skipping / removing final file because marked as PENDING", logical_filepath)
|
||||
|
||||
@@ -15,6 +15,12 @@ import configuration
|
||||
import preplots
|
||||
from datastore import Datastore
|
||||
|
||||
def preplots_sorter (preplot):
|
||||
rank = {
|
||||
"x-sl+csv": 10
|
||||
}
|
||||
return rank.get(preplot.get("type"), 0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
print("Connecting to database")
|
||||
@@ -28,7 +34,10 @@ if __name__ == '__main__':
|
||||
for survey in surveys:
|
||||
print(f'Survey: {survey["id"]} ({survey["schema"]})')
|
||||
db.set_survey(survey["schema"])
|
||||
for file in survey["preplots"]:
|
||||
|
||||
# We sort the preplots so that ancillary line info always comes last,
|
||||
# after the actual line + point data has been imported
|
||||
for file in sorted(survey["preplots"], key=preplots_sorter):
|
||||
realpath = configuration.translate_path(file["path"])
|
||||
|
||||
print(f"Preplot: {file['path']}")
|
||||
@@ -48,7 +57,10 @@ if __name__ == '__main__':
|
||||
|
||||
if type(preplot) is list:
|
||||
print("Saving to DB")
|
||||
db.save_preplots(preplot, file["path"], file["class"], survey["epsg"], file)
|
||||
if file.get("type") == "x-sl+csv":
|
||||
db.save_preplot_line_info(preplot, file["path"], file)
|
||||
else:
|
||||
db.save_preplots(preplot, file["path"], file["class"], survey["epsg"], file)
|
||||
elif type(preplot) is str:
|
||||
print(preplot)
|
||||
else:
|
||||
|
||||
@@ -15,6 +15,7 @@ import re
|
||||
import time
|
||||
import configuration
|
||||
import p111
|
||||
import fwr
|
||||
from datastore import Datastore
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -38,8 +39,11 @@ if __name__ == '__main__':
|
||||
print("No raw P1/11 configuration")
|
||||
exit(0)
|
||||
|
||||
pattern = raw_p111["pattern"]
|
||||
rx = re.compile(pattern["regex"])
|
||||
lineNameInfo = raw_p111.get("lineNameInfo")
|
||||
pattern = raw_p111.get("pattern")
|
||||
rx = None
|
||||
if pattern and pattern.get("regex"):
|
||||
rx = re.compile(pattern["regex"])
|
||||
|
||||
if "ntbp" in survey["raw"]:
|
||||
ntbpRx = re.compile(survey["raw"]["ntbp"]["pattern"]["regex"])
|
||||
@@ -68,16 +72,38 @@ if __name__ == '__main__':
|
||||
|
||||
print("Importing")
|
||||
|
||||
match = rx.match(os.path.basename(logical_filepath))
|
||||
if not match:
|
||||
error_message = f"File path not matching the expected format! ({logical_filepath} ~ {pattern['regex']})"
|
||||
print(error_message, file=sys.stderr)
|
||||
print("This file will be ignored!")
|
||||
continue
|
||||
if rx:
|
||||
match = rx.match(os.path.basename(logical_filepath))
|
||||
if not match:
|
||||
error_message = f"File path not matching the expected format! ({logical_filepath} ~ {pattern['regex']})"
|
||||
print(error_message, file=sys.stderr)
|
||||
print("This file will be ignored!")
|
||||
continue
|
||||
|
||||
file_info = dict(zip(pattern["captures"], match.groups()))
|
||||
file_info["meta"] = {}
|
||||
file_info = dict(zip(pattern["captures"], match.groups()))
|
||||
file_info["meta"] = {}
|
||||
|
||||
if lineNameInfo:
|
||||
basename = os.path.basename(physical_filepath)
|
||||
fields = lineNameInfo.get("fields", {})
|
||||
fixed = lineNameInfo.get("fixed")
|
||||
try:
|
||||
parsed_line = fwr.parse_line(basename, fields, fixed)
|
||||
except ValueError as err:
|
||||
parsed_line = "Line format error: " + str(err)
|
||||
if type(parsed_line) == str:
|
||||
print(parsed_line, file=sys.stderr)
|
||||
print("This file will be ignored!")
|
||||
continue
|
||||
|
||||
file_info = {}
|
||||
file_info["sequence"] = parsed_line["sequence"]
|
||||
file_info["line"] = parsed_line["line"]
|
||||
del(parsed_line["sequence"])
|
||||
del(parsed_line["line"])
|
||||
file_info["meta"] = {
|
||||
"fileInfo": parsed_line
|
||||
}
|
||||
p111_data = p111.from_file(physical_filepath)
|
||||
|
||||
print("Saving")
|
||||
|
||||
@@ -15,6 +15,7 @@ import re
|
||||
import time
|
||||
import configuration
|
||||
import smsrc
|
||||
import fwr
|
||||
from datastore import Datastore
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -33,17 +34,21 @@ if __name__ == '__main__':
|
||||
db.set_survey(survey["schema"])
|
||||
|
||||
try:
|
||||
raw_smsrc = survey["raw"]["smsrc"]
|
||||
raw_smsrc = survey["raw"]["source"]["smsrc"]["header"]
|
||||
except KeyError:
|
||||
print("No SmartSource data configuration")
|
||||
continue
|
||||
|
||||
flags = 0
|
||||
if "flags" in raw_smsrc:
|
||||
configuration.rxflags(raw_smsrc["flags"])
|
||||
# NOTE I've no idea what this is 🤔
|
||||
# flags = 0
|
||||
# if "flags" in raw_smsrc:
|
||||
# configuration.rxflags(raw_smsrc["flags"])
|
||||
|
||||
pattern = raw_smsrc["pattern"]
|
||||
rx = re.compile(pattern["regex"], flags)
|
||||
lineNameInfo = raw_smsrc.get("lineNameInfo")
|
||||
pattern = raw_smsrc.get("pattern")
|
||||
rx = None
|
||||
if pattern and pattern.get("regex"):
|
||||
rx = re.compile(pattern["regex"])
|
||||
|
||||
for fileprefix in raw_smsrc["paths"]:
|
||||
realprefix = configuration.translate_path(fileprefix)
|
||||
@@ -64,14 +69,39 @@ if __name__ == '__main__':
|
||||
|
||||
print("Importing")
|
||||
|
||||
match = rx.match(os.path.basename(logical_filepath))
|
||||
if not match:
|
||||
error_message = f"File path not matching the expected format! ({logical_filepath} ~ {pattern['regex']})"
|
||||
print(error_message, file=sys.stderr)
|
||||
print("This file will be ignored!")
|
||||
continue
|
||||
if rx:
|
||||
match = rx.match(os.path.basename(logical_filepath))
|
||||
if not match:
|
||||
error_message = f"File path not matching the expected format! ({logical_filepath} ~ {pattern['regex']})"
|
||||
print(error_message, file=sys.stderr)
|
||||
print("This file will be ignored!")
|
||||
continue
|
||||
|
||||
file_info = dict(zip(pattern["captures"], match.groups()))
|
||||
file_info = dict(zip(pattern["captures"], match.groups()))
|
||||
file_info["meta"] = {}
|
||||
|
||||
|
||||
if lineNameInfo:
|
||||
basename = os.path.basename(physical_filepath)
|
||||
fields = lineNameInfo.get("fields", {})
|
||||
fixed = lineNameInfo.get("fixed")
|
||||
try:
|
||||
parsed_line = fwr.parse_line(basename, fields, fixed)
|
||||
except ValueError as err:
|
||||
parsed_line = "Line format error: " + str(err)
|
||||
if type(parsed_line) == str:
|
||||
print(parsed_line, file=sys.stderr)
|
||||
print("This file will be ignored!")
|
||||
continue
|
||||
|
||||
file_info = {}
|
||||
file_info["sequence"] = parsed_line["sequence"]
|
||||
file_info["line"] = parsed_line["line"]
|
||||
del(parsed_line["sequence"])
|
||||
del(parsed_line["line"])
|
||||
file_info["meta"] = {
|
||||
"fileInfo": parsed_line
|
||||
}
|
||||
|
||||
smsrc_records = smsrc.from_file(physical_filepath)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ P1/11 parsing functions.
|
||||
import math
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from parse_fwr import parse_fwr
|
||||
|
||||
def _int (string):
|
||||
return int(float(string))
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
def parse_fwr (string, widths, start=0):
|
||||
"""Parse a fixed-width record.
|
||||
|
||||
string: the string to parse.
|
||||
widths: a list of record widths. A negative width denotes a field to be skipped.
|
||||
start: optional start index.
|
||||
|
||||
Returns a list of strings.
|
||||
"""
|
||||
results = []
|
||||
current_index = start
|
||||
for width in widths:
|
||||
if width > 0:
|
||||
results.append(string[current_index : current_index + width])
|
||||
current_index += width
|
||||
else:
|
||||
current_index -= width
|
||||
|
||||
return results
|
||||
@@ -1,15 +1,51 @@
|
||||
import sps
|
||||
import fwr
|
||||
import delimited
|
||||
|
||||
"""
|
||||
Preplot importing functions.
|
||||
"""
|
||||
|
||||
|
||||
def is_fixed_width (file):
|
||||
fixed_width_types = [ "sps1", "sps21", "p190", "fixed-width" ]
|
||||
return type(file) == dict and "type" in file and file["type"] in fixed_width_types
|
||||
|
||||
def is_delimited (file):
|
||||
delimited_types = [ "csv", "p111", "x-sl+csv" ]
|
||||
return type(file) == dict and "type" in file and file["type"] in delimited_types
|
||||
|
||||
def from_file (file, realpath = None):
|
||||
"""
|
||||
Return a list of dicts, where each dict has the structure:
|
||||
{
|
||||
"line_name": <int>,
|
||||
"points": [
|
||||
{
|
||||
"line_name": <int>,
|
||||
"point_number": <int>,
|
||||
"easting": <float>,
|
||||
"northing": <float>
|
||||
},
|
||||
…
|
||||
]
|
||||
}
|
||||
On error, return a string describing the error condition.
|
||||
"""
|
||||
|
||||
filepath = realpath or file["path"]
|
||||
if not "type" in file or file["type"] == "sps":
|
||||
records = sps.from_file(filepath, file["format"] if "format" in file else None )
|
||||
if is_fixed_width(file):
|
||||
records = fwr.from_file(filepath, file)
|
||||
elif is_delimited(file):
|
||||
records = delimited.from_file(filepath, file)
|
||||
else:
|
||||
return "Not an SPS file"
|
||||
return "Unrecognised file format"
|
||||
|
||||
if type(records) == str:
|
||||
# This is an error message
|
||||
return records
|
||||
|
||||
if file.get("type") == "x-sl+csv":
|
||||
return records
|
||||
|
||||
lines = []
|
||||
line_names = set([r["line_name"] for r in records])
|
||||
|
||||
51
bin/sps.py
51
bin/sps.py
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
"""
|
||||
SPS importing functions.
|
||||
|
||||
And by SPS, we mean more or less any line-delimited, fixed-width record format.
|
||||
"""
|
||||
|
||||
import builtins
|
||||
from parse_fwr import parse_fwr
|
||||
|
||||
def int (v):
|
||||
return builtins.int(float(v))
|
||||
|
||||
def parse_line (string, spec):
|
||||
"""Parse a line from an SPS file."""
|
||||
names = spec["names"]
|
||||
widths = spec["widths"]
|
||||
normalisers = spec["normalisers"]
|
||||
record = [ t[0](t[1]) for t in zip(normalisers, parse_fwr(string, widths)) ]
|
||||
return dict(zip(names, record))
|
||||
|
||||
def from_file(path, spec = None):
|
||||
if spec is None:
|
||||
spec = {
|
||||
"names": [ "line_name", "point_number", "easting", "northing" ],
|
||||
"widths": [ -1, 10, 10, -25, 10, 10 ],
|
||||
"normalisers": [ int, int, float, float ]
|
||||
}
|
||||
else:
|
||||
normaliser_tokens = [ "int", "float", "str", "bool" ]
|
||||
spec["normalisers"] = [ eval(t) for t in spec["types"] if t in normaliser_tokens ]
|
||||
|
||||
records = []
|
||||
with open(path) as fd:
|
||||
cnt = 0
|
||||
line = fd.readline()
|
||||
while line:
|
||||
cnt = cnt+1
|
||||
|
||||
if line == "EOF":
|
||||
break
|
||||
|
||||
record = parse_line(line, spec)
|
||||
if record is not None:
|
||||
records.append(record)
|
||||
|
||||
line = fd.readline()
|
||||
|
||||
del spec["normalisers"]
|
||||
return records
|
||||
@@ -1,5 +1,5 @@
|
||||
\connect dougal
|
||||
|
||||
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.4.2"}')
|
||||
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.4.5"}')
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = public.info.value || '{"db_schema": "0.4.2"}' WHERE public.info.key = 'version';
|
||||
SET value = public.info.value || '{"db_schema": "0.4.5"}' WHERE public.info.key = 'version';
|
||||
|
||||
@@ -399,6 +399,62 @@ $$;
|
||||
|
||||
ALTER FUNCTION _SURVEY__TEMPLATE_.clear_shot_qc() OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: event_log_uid_seq; Type: SEQUENCE; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE SEQUENCE _SURVEY__TEMPLATE_.event_log_uid_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE _SURVEY__TEMPLATE_.event_log_uid_seq OWNER TO postgres;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
--
|
||||
-- Name: event_log_full; Type: TABLE; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE _SURVEY__TEMPLATE_.event_log_full (
|
||||
uid integer DEFAULT nextval('_SURVEY__TEMPLATE_.event_log_uid_seq'::regclass) NOT NULL,
|
||||
id integer NOT NULL,
|
||||
tstamp timestamp with time zone,
|
||||
sequence integer,
|
||||
point integer,
|
||||
remarks text DEFAULT ''::text NOT NULL,
|
||||
labels text[] DEFAULT ARRAY[]::text[] NOT NULL,
|
||||
meta jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
validity tstzrange NOT NULL,
|
||||
CONSTRAINT event_log_full_check CHECK ((((tstamp IS NOT NULL) AND (sequence IS NOT NULL) AND (point IS NOT NULL)) OR ((tstamp IS NOT NULL) AND (sequence IS NULL) AND (point IS NULL)) OR ((tstamp IS NULL) AND (sequence IS NOT NULL) AND (point IS NOT NULL)))),
|
||||
CONSTRAINT event_log_full_validity_check CHECK ((NOT isempty(validity)))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE _SURVEY__TEMPLATE_.event_log_full OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: event_log_changes(timestamp with time zone); Type: FUNCTION; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE FUNCTION _SURVEY__TEMPLATE_.event_log_changes(ts0 timestamp with time zone) RETURNS SETOF _SURVEY__TEMPLATE_.event_log_full
|
||||
LANGUAGE sql
|
||||
AS $$
|
||||
SELECT *
|
||||
FROM event_log_full
|
||||
WHERE lower(validity) > ts0 OR upper(validity) IS NOT NULL AND upper(validity) > ts0
|
||||
ORDER BY lower(validity);
|
||||
$$;
|
||||
|
||||
|
||||
ALTER FUNCTION _SURVEY__TEMPLATE_.event_log_changes(ts0 timestamp with time zone) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: event_log_full_insert(); Type: FUNCTION; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
||||
--
|
||||
@@ -881,46 +937,6 @@ $$;
|
||||
|
||||
ALTER FUNCTION _SURVEY__TEMPLATE_.ij_error(line double precision, point double precision, geom public.geometry) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: event_log_uid_seq; Type: SEQUENCE; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE SEQUENCE _SURVEY__TEMPLATE_.event_log_uid_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE _SURVEY__TEMPLATE_.event_log_uid_seq OWNER TO postgres;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
--
|
||||
-- Name: event_log_full; Type: TABLE; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE _SURVEY__TEMPLATE_.event_log_full (
|
||||
uid integer DEFAULT nextval('_SURVEY__TEMPLATE_.event_log_uid_seq'::regclass) NOT NULL,
|
||||
id integer NOT NULL,
|
||||
tstamp timestamp with time zone,
|
||||
sequence integer,
|
||||
point integer,
|
||||
remarks text DEFAULT ''::text NOT NULL,
|
||||
labels text[] DEFAULT ARRAY[]::text[] NOT NULL,
|
||||
meta jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
validity tstzrange NOT NULL,
|
||||
CONSTRAINT event_log_full_check CHECK ((((tstamp IS NOT NULL) AND (sequence IS NOT NULL) AND (point IS NOT NULL)) OR ((tstamp IS NOT NULL) AND (sequence IS NULL) AND (point IS NULL)) OR ((tstamp IS NULL) AND (sequence IS NOT NULL) AND (point IS NOT NULL)))),
|
||||
CONSTRAINT event_log_full_validity_check CHECK ((NOT isempty(validity)))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE _SURVEY__TEMPLATE_.event_log_full OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: event_log; Type: VIEW; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
||||
--
|
||||
@@ -1519,9 +1535,9 @@ CREATE VIEW _SURVEY__TEMPLATE_.final_lines_summary AS
|
||||
s.ts1,
|
||||
(s.ts1 - s.ts0) AS duration,
|
||||
s.num_points,
|
||||
( SELECT count(*) AS count
|
||||
FROM _SURVEY__TEMPLATE_.missing_sequence_final_points
|
||||
WHERE missing_sequence_final_points.sequence = s.sequence) AS missing_shots,
|
||||
(( SELECT count(*) AS count
|
||||
FROM _SURVEY__TEMPLATE_.preplot_points
|
||||
WHERE ((preplot_points.line = fl.line) AND (((preplot_points.point >= s.fsp) AND (preplot_points.point <= s.lsp)) OR ((preplot_points.point >= s.lsp) AND (preplot_points.point <= s.fsp))))) - s.num_points) AS missing_shots,
|
||||
s.length,
|
||||
s.azimuth,
|
||||
fl.remarks,
|
||||
@@ -2077,10 +2093,10 @@ CREATE VIEW _SURVEY__TEMPLATE_.preplot_summary AS
|
||||
ALTER TABLE _SURVEY__TEMPLATE_.preplot_summary OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: project_summary; Type: VIEW; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
||||
-- Name: project_summary; Type: MATERIALIZED VIEW; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE VIEW _SURVEY__TEMPLATE_.project_summary AS
|
||||
CREATE MATERIALIZED VIEW _SURVEY__TEMPLATE_.project_summary AS
|
||||
WITH fls AS (
|
||||
SELECT avg((final_lines_summary.duration / ((final_lines_summary.num_points - 1))::double precision)) AS shooting_rate,
|
||||
avg((final_lines_summary.length / date_part('epoch'::text, final_lines_summary.duration))) AS speed,
|
||||
@@ -2123,7 +2139,8 @@ CREATE VIEW _SURVEY__TEMPLATE_.project_summary AS
|
||||
fls.speed AS shooting_rate
|
||||
FROM _SURVEY__TEMPLATE_.preplot_summary ps,
|
||||
fls,
|
||||
project;
|
||||
project
|
||||
WITH NO DATA;
|
||||
|
||||
|
||||
ALTER TABLE _SURVEY__TEMPLATE_.project_summary OWNER TO postgres;
|
||||
@@ -2168,9 +2185,9 @@ CREATE VIEW _SURVEY__TEMPLATE_.raw_lines_summary AS
|
||||
(s.ts1 - s.ts0) AS duration,
|
||||
s.num_points,
|
||||
s.num_preplots,
|
||||
(SELECT count(*) AS count
|
||||
FROM _SURVEY__TEMPLATE_.missing_sequence_raw_points
|
||||
WHERE missing_sequence_raw_points.sequence = s.sequence) AS missing_shots,
|
||||
(( SELECT count(*) AS count
|
||||
FROM _SURVEY__TEMPLATE_.preplot_points
|
||||
WHERE ((preplot_points.line = rl.line) AND (((preplot_points.point >= s.fsp) AND (preplot_points.point <= s.lsp)) OR ((preplot_points.point >= s.lsp) AND (preplot_points.point <= s.fsp))))) - s.num_preplots) AS missing_shots,
|
||||
s.length,
|
||||
s.azimuth,
|
||||
rl.remarks,
|
||||
|
||||
164
etc/db/upgrades/upgrade33-v0.5.0-sailline-ancillary-data.sql
Normal file
164
etc/db/upgrades/upgrade33-v0.5.0-sailline-ancillary-data.sql
Normal file
@@ -0,0 +1,164 @@
|
||||
-- Sailline ancillary data
|
||||
--
|
||||
-- New schema version: 0.5.0
|
||||
--
|
||||
-- ATTENTION:
|
||||
--
|
||||
-- ENSURE YOU HAVE BACKED UP THE DATABASE BEFORE RUNNING THIS SCRIPT.
|
||||
--
|
||||
--
|
||||
-- NOTE: This upgrade affects all schemas in the database.
|
||||
-- NOTE: Each application starts a transaction, which must be committed
|
||||
-- or rolled back.
|
||||
--
|
||||
-- Issue #264 calls for associating sail and acquisition lines as well
|
||||
-- as indicating expected acquisition direction, and other data which
|
||||
-- cannot be provided via standard import formats such as SPS or P1/90.
|
||||
--
|
||||
-- We support this via an additional table that holds most of the required
|
||||
-- data. This data can simply be inferred from regular preplots, e.g., line
|
||||
-- direction can be deduced from preplot point order, and sail / source
|
||||
-- line offsets can be taken from P1/90 headers or from a configuration
|
||||
-- parameter. Alternatively, and in preference, the data can be provided
|
||||
-- explicitly, which is what issue #264 asks for.
|
||||
--
|
||||
-- In principle, this makes at least some of the attributes of `preplot_lines`
|
||||
-- redundant (at least `incr` and `ntba`) but we will leave them there for
|
||||
-- the time being as technical debt.
|
||||
--
|
||||
-- To apply, run as the dougal user:
|
||||
--
|
||||
-- psql <<EOF
|
||||
-- \i $THIS_FILE
|
||||
-- COMMIT;
|
||||
-- EOF
|
||||
--
|
||||
-- NOTE: It can be applied multiple times without ill effect.
|
||||
--
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.show_notice (notice text) AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE '%', notice;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade_survey_schema (schema_name text) AS $outer$
|
||||
BEGIN
|
||||
|
||||
RAISE NOTICE 'Updating schema %', schema_name;
|
||||
-- We need to set the search path because some of the trigger
|
||||
-- functions reference other tables in survey schemas assuming
|
||||
-- they are in the search path.
|
||||
EXECUTE format('SET search_path TO %I,public', schema_name);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS preplot_saillines
|
||||
(
|
||||
sailline integer NOT NULL,
|
||||
line integer NOT NULL,
|
||||
sailline_class character(1) NOT NULL,
|
||||
line_class character(1) NOT NULL,
|
||||
incr boolean NOT NULL DEFAULT true,
|
||||
ntba boolean NOT NULL DEFAULT false,
|
||||
remarks text NOT NULL DEFAULT '',
|
||||
meta jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
hash text NULL, -- Theoretically the info in this table could all be inferred.
|
||||
PRIMARY KEY (sailline, sailline_class, line, line_class, incr),
|
||||
CONSTRAINT fk_sailline FOREIGN KEY (sailline, sailline_class)
|
||||
REFERENCES preplot_lines (line, class)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_line FOREIGN KEY (line, line_class)
|
||||
REFERENCES preplot_lines (line, class)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_hash FOREIGN KEY (hash)
|
||||
REFERENCES files (hash) MATCH SIMPLE
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
CHECK (sailline_class = 'V' AND sailline_class != line_class)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE preplot_saillines
|
||||
IS 'We explicitly associate each preplot sailline (aka vessel line) with zero or more source lines. This information can be inferred from preplot files, e.g., via a sailline offset value, or explicitly provided.';
|
||||
|
||||
-- Let us copy whatever information we can from existing tables or views
|
||||
|
||||
INSERT INTO preplot_saillines
|
||||
(sailline, line, sailline_class, line_class, incr, ntba, remarks, meta)
|
||||
SELECT DISTINCT
|
||||
sailline, psp.line, 'V' sailline_class, psp.class line_class, pl.incr, pl.ntba, pl.remarks, pl.meta
|
||||
FROM preplot_saillines_points psp
|
||||
INNER JOIN preplot_lines pl ON psp.sailline = pl.line AND pl.class = 'V'
|
||||
ORDER BY sailline
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- We need to recreate the preplot_saillines_points view
|
||||
|
||||
CREATE OR REPLACE VIEW preplot_saillines_points AS
|
||||
SELECT psl.sailline,
|
||||
psl.ntba AS sailline_ntba,
|
||||
psl.line,
|
||||
pps.point,
|
||||
pps.class,
|
||||
pps.ntba,
|
||||
pps.geometry,
|
||||
pps.meta
|
||||
FROM preplot_saillines psl
|
||||
INNER JOIN preplot_points pps
|
||||
ON psl.line = pps.line AND psl.line_class = pps.class;
|
||||
|
||||
|
||||
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade () AS $outer$
|
||||
DECLARE
|
||||
row RECORD;
|
||||
current_db_version TEXT;
|
||||
BEGIN
|
||||
|
||||
SELECT value->>'db_schema' INTO current_db_version FROM public.info WHERE key = 'version';
|
||||
|
||||
IF current_db_version >= '0.5.0' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Patch already applied';
|
||||
END IF;
|
||||
|
||||
IF current_db_version != '0.4.5' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Invalid database version: ' || current_db_version,
|
||||
HINT='Ensure all previous patches have been applied.';
|
||||
END IF;
|
||||
|
||||
FOR row IN
|
||||
SELECT schema_name FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'survey_%'
|
||||
ORDER BY schema_name
|
||||
LOOP
|
||||
CALL pg_temp.upgrade_survey_schema(row.schema_name);
|
||||
END LOOP;
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CALL pg_temp.upgrade();
|
||||
|
||||
CALL pg_temp.show_notice('Cleaning up');
|
||||
DROP PROCEDURE pg_temp.upgrade_survey_schema (schema_name text);
|
||||
DROP PROCEDURE pg_temp.upgrade ();
|
||||
|
||||
CALL pg_temp.show_notice('Updating db_schema version');
|
||||
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.5.0"}')
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = public.info.value || '{"db_schema": "0.5.0"}' WHERE public.info.key = 'version';
|
||||
|
||||
|
||||
CALL pg_temp.show_notice('All done. You may now run "COMMIT;" to persist the changes');
|
||||
DROP PROCEDURE pg_temp.show_notice (notice text);
|
||||
|
||||
--
|
||||
--NOTE Run `COMMIT;` now if all went well
|
||||
--
|
||||
119
etc/db/upgrades/upgrade34-v0.5.1-fix-sequences-detail-view.sql
Normal file
119
etc/db/upgrades/upgrade34-v0.5.1-fix-sequences-detail-view.sql
Normal file
@@ -0,0 +1,119 @@
|
||||
-- Sailline ancillary data
|
||||
--
|
||||
-- New schema version: 0.5.1
|
||||
--
|
||||
-- ATTENTION:
|
||||
--
|
||||
-- ENSURE YOU HAVE BACKED UP THE DATABASE BEFORE RUNNING THIS SCRIPT.
|
||||
--
|
||||
--
|
||||
-- NOTE: This upgrade affects all schemas in the database.
|
||||
-- NOTE: Each application starts a transaction, which must be committed
|
||||
-- or rolled back.
|
||||
--
|
||||
-- The sequences_detail view wrongly associates source lines and shot
|
||||
-- points when it should be associating saillines and shot points instead.
|
||||
--
|
||||
-- This updates fixes that issue (#307).
|
||||
--
|
||||
-- To apply, run as the dougal user:
|
||||
--
|
||||
-- psql <<EOF
|
||||
-- \i $THIS_FILE
|
||||
-- COMMIT;
|
||||
-- EOF
|
||||
--
|
||||
-- NOTE: It can be applied multiple times without ill effect.
|
||||
--
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.show_notice (notice text) AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE '%', notice;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade_survey_schema (schema_name text) AS $outer$
|
||||
BEGIN
|
||||
|
||||
RAISE NOTICE 'Updating schema %', schema_name;
|
||||
-- We need to set the search path because some of the trigger
|
||||
-- functions reference other tables in survey schemas assuming
|
||||
-- they are in the search path.
|
||||
EXECUTE format('SET search_path TO %I,public', schema_name);
|
||||
|
||||
CREATE OR REPLACE VIEW sequences_detail
|
||||
AS
|
||||
SELECT rl.sequence,
|
||||
rl.line AS sailline,
|
||||
rs.line,
|
||||
rs.point,
|
||||
rs.tstamp,
|
||||
rs.objref AS objrefraw,
|
||||
fs.objref AS objreffinal,
|
||||
st_transform(pp.geometry, 4326) AS geometrypreplot,
|
||||
st_transform(rs.geometry, 4326) AS geometryraw,
|
||||
st_transform(fs.geometry, 4326) AS geometryfinal,
|
||||
ij_error(rs.line::double precision, rs.point::double precision, rs.geometry) AS errorraw,
|
||||
ij_error(rs.line::double precision, rs.point::double precision, fs.geometry) AS errorfinal,
|
||||
json_build_object('preplot', pp.meta, 'raw', rs.meta, 'final', fs.meta) AS meta
|
||||
FROM raw_lines rl
|
||||
INNER JOIN preplot_saillines psl ON rl.line = psl.sailline
|
||||
INNER JOIN raw_shots rs ON rs.sequence = rl.sequence AND rs.line = psl.line
|
||||
INNER JOIN preplot_points pp ON psl.line = pp.line AND psl.line_class = pp.class AND rs.point = pp.point
|
||||
LEFT JOIN final_shots fs ON rl.sequence = fs.sequence AND rs.point = fs.point;
|
||||
|
||||
ALTER TABLE sequences_detail
|
||||
OWNER TO postgres;
|
||||
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade () AS $outer$
|
||||
DECLARE
|
||||
row RECORD;
|
||||
current_db_version TEXT;
|
||||
BEGIN
|
||||
|
||||
SELECT value->>'db_schema' INTO current_db_version FROM public.info WHERE key = 'version';
|
||||
|
||||
IF current_db_version >= '0.5.1' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Patch already applied';
|
||||
END IF;
|
||||
|
||||
IF current_db_version != '0.5.0' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Invalid database version: ' || current_db_version,
|
||||
HINT='Ensure all previous patches have been applied.';
|
||||
END IF;
|
||||
|
||||
FOR row IN
|
||||
SELECT schema_name FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'survey_%'
|
||||
ORDER BY schema_name
|
||||
LOOP
|
||||
CALL pg_temp.upgrade_survey_schema(row.schema_name);
|
||||
END LOOP;
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CALL pg_temp.upgrade();
|
||||
|
||||
CALL pg_temp.show_notice('Cleaning up');
|
||||
DROP PROCEDURE pg_temp.upgrade_survey_schema (schema_name text);
|
||||
DROP PROCEDURE pg_temp.upgrade ();
|
||||
|
||||
CALL pg_temp.show_notice('Updating db_schema version');
|
||||
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.5.1"}')
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = public.info.value || '{"db_schema": "0.5.1"}' WHERE public.info.key = 'version';
|
||||
|
||||
|
||||
CALL pg_temp.show_notice('All done. You may now run "COMMIT;" to persist the changes');
|
||||
DROP PROCEDURE pg_temp.show_notice (notice text);
|
||||
|
||||
--
|
||||
--NOTE Run `COMMIT;` now if all went well
|
||||
--
|
||||
@@ -0,0 +1,145 @@
|
||||
-- Fix preplot_lines_summary view
|
||||
--
|
||||
-- New schema version: 0.5.2
|
||||
--
|
||||
-- WARNING: This update is buggy and does not give the desired
|
||||
-- results. Schema version 0.5.4 fixes this.
|
||||
--
|
||||
-- ATTENTION:
|
||||
--
|
||||
-- ENSURE YOU HAVE BACKED UP THE DATABASE BEFORE RUNNING THIS SCRIPT.
|
||||
--
|
||||
--
|
||||
-- NOTE: This upgrade affects all schemas in the database.
|
||||
-- NOTE: Each application starts a transaction, which must be committed
|
||||
-- or rolled back.
|
||||
--
|
||||
-- Following introduction of `preplot_saillines` (0.5.0), the incr and
|
||||
-- ntba statuses are stored in a separate table, not in `preplot_lines`
|
||||
-- (TODO: a future upgrade should remove those columns from `preplot_lines`)
|
||||
--
|
||||
-- Now any views referencing `incr` and `ntba` must be updated to point to
|
||||
-- the new location of those attributes.
|
||||
--
|
||||
-- This update fixes #312.
|
||||
--
|
||||
-- To apply, run as the dougal user:
|
||||
--
|
||||
-- psql <<EOF
|
||||
-- \i $THIS_FILE
|
||||
-- COMMIT;
|
||||
-- EOF
|
||||
--
|
||||
-- NOTE: It can be applied multiple times without ill effect.
|
||||
--
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.show_notice (notice text) AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE '%', notice;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade_survey_schema (schema_name text) AS $outer$
|
||||
BEGIN
|
||||
|
||||
RAISE NOTICE 'Updating schema %', schema_name;
|
||||
-- We need to set the search path because some of the trigger
|
||||
-- functions reference other tables in survey schemas assuming
|
||||
-- they are in the search path.
|
||||
EXECUTE format('SET search_path TO %I,public', schema_name);
|
||||
|
||||
CREATE OR REPLACE VIEW preplot_lines_summary
|
||||
AS
|
||||
WITH summary AS (
|
||||
SELECT DISTINCT pp.line, pp.class,
|
||||
first_value(pp.point) OVER w AS p0,
|
||||
last_value(pp.point) OVER w AS p1,
|
||||
count(pp.point) OVER w AS num_points,
|
||||
st_distance(first_value(pp.geometry) OVER w, last_value(pp.geometry) OVER w) AS length,
|
||||
st_azimuth(first_value(pp.geometry) OVER w, last_value(pp.geometry) OVER w) * 180::double precision / pi() AS azimuth0,
|
||||
st_azimuth(last_value(pp.geometry) OVER w, first_value(pp.geometry) OVER w) * 180::double precision / pi() AS azimuth1
|
||||
FROM preplot_points pp
|
||||
WHERE pp.class = 'V'::bpchar
|
||||
WINDOW w AS (PARTITION BY pp.line ORDER BY pp.point ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
|
||||
)
|
||||
SELECT psl.line,
|
||||
CASE
|
||||
WHEN psl.incr THEN s.p0
|
||||
ELSE s.p1
|
||||
END AS fsp,
|
||||
CASE
|
||||
WHEN psl.incr THEN s.p1
|
||||
ELSE s.p0
|
||||
END AS lsp,
|
||||
s.num_points,
|
||||
s.length,
|
||||
CASE
|
||||
WHEN psl.incr THEN s.azimuth0
|
||||
ELSE s.azimuth1
|
||||
END AS azimuth,
|
||||
psl.incr,
|
||||
psl.remarks
|
||||
FROM summary s
|
||||
JOIN preplot_saillines psl ON psl.sailline_class = s.class AND s.line = psl.line
|
||||
ORDER BY psl.line, incr;
|
||||
|
||||
|
||||
ALTER TABLE preplot_lines_summary
|
||||
OWNER TO postgres;
|
||||
COMMENT ON VIEW preplot_lines_summary
|
||||
IS 'Summarises ''V'' (vessel sailline) preplot lines.';
|
||||
|
||||
|
||||
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade () AS $outer$
|
||||
DECLARE
|
||||
row RECORD;
|
||||
current_db_version TEXT;
|
||||
BEGIN
|
||||
|
||||
SELECT value->>'db_schema' INTO current_db_version FROM public.info WHERE key = 'version';
|
||||
|
||||
IF current_db_version >= '0.5.2' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Patch already applied';
|
||||
END IF;
|
||||
|
||||
IF current_db_version != '0.5.1' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Invalid database version: ' || current_db_version,
|
||||
HINT='Ensure all previous patches have been applied.';
|
||||
END IF;
|
||||
|
||||
FOR row IN
|
||||
SELECT schema_name FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'survey_%'
|
||||
ORDER BY schema_name
|
||||
LOOP
|
||||
CALL pg_temp.upgrade_survey_schema(row.schema_name);
|
||||
END LOOP;
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CALL pg_temp.upgrade();
|
||||
|
||||
CALL pg_temp.show_notice('Cleaning up');
|
||||
DROP PROCEDURE pg_temp.upgrade_survey_schema (schema_name text);
|
||||
DROP PROCEDURE pg_temp.upgrade ();
|
||||
|
||||
CALL pg_temp.show_notice('Updating db_schema version');
|
||||
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.5.2"}')
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = public.info.value || '{"db_schema": "0.5.2"}' WHERE public.info.key = 'version';
|
||||
|
||||
|
||||
CALL pg_temp.show_notice('All done. You may now run "COMMIT;" to persist the changes');
|
||||
DROP PROCEDURE pg_temp.show_notice (notice text);
|
||||
|
||||
--
|
||||
--NOTE Run `COMMIT;` now if all went well
|
||||
--
|
||||
@@ -0,0 +1,132 @@
|
||||
-- Fix final_lines_summary view
|
||||
--
|
||||
-- New schema version: 0.5.3
|
||||
--
|
||||
-- ATTENTION:
|
||||
--
|
||||
-- ENSURE YOU HAVE BACKED UP THE DATABASE BEFORE RUNNING THIS SCRIPT.
|
||||
--
|
||||
--
|
||||
-- NOTE: This upgrade affects all schemas in the database.
|
||||
-- NOTE: Each application starts a transaction, which must be committed
|
||||
-- or rolled back.
|
||||
--
|
||||
-- This fixes a long-standing bug, where if the sail and source lines are
|
||||
-- the same, the number of missing shots will be miscounted.
|
||||
--
|
||||
-- This update fixes #313.
|
||||
--
|
||||
-- To apply, run as the dougal user:
|
||||
--
|
||||
-- psql <<EOF
|
||||
-- \i $THIS_FILE
|
||||
-- COMMIT;
|
||||
-- EOF
|
||||
--
|
||||
-- NOTE: It can be applied multiple times without ill effect.
|
||||
--
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.show_notice (notice text) AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE '%', notice;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade_survey_schema (schema_name text) AS $outer$
|
||||
BEGIN
|
||||
|
||||
RAISE NOTICE 'Updating schema %', schema_name;
|
||||
-- We need to set the search path because some of the trigger
|
||||
-- functions reference other tables in survey schemas assuming
|
||||
-- they are in the search path.
|
||||
EXECUTE format('SET search_path TO %I,public', schema_name);
|
||||
|
||||
CREATE OR REPLACE VIEW final_lines_summary
|
||||
AS
|
||||
WITH summary AS (
|
||||
SELECT DISTINCT fs.sequence,
|
||||
first_value(fs.point) OVER w AS fsp,
|
||||
last_value(fs.point) OVER w AS lsp,
|
||||
first_value(fs.tstamp) OVER w AS ts0,
|
||||
last_value(fs.tstamp) OVER w AS ts1,
|
||||
count(fs.point) OVER w AS num_points,
|
||||
count(pp.point) OVER w AS num_preplots,
|
||||
st_distance(first_value(fs.geometry) OVER w, last_value(fs.geometry) OVER w) AS length,
|
||||
st_azimuth(first_value(fs.geometry) OVER w, last_value(fs.geometry) OVER w) * 180::double precision / pi() AS azimuth
|
||||
FROM final_shots fs
|
||||
LEFT JOIN preplot_points pp USING (line, point)
|
||||
WINDOW w AS (PARTITION BY fs.sequence ORDER BY fs.tstamp ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
|
||||
)
|
||||
SELECT fl.sequence,
|
||||
fl.line,
|
||||
s.fsp,
|
||||
s.lsp,
|
||||
s.ts0,
|
||||
s.ts1,
|
||||
s.ts1 - s.ts0 AS duration,
|
||||
s.num_points,
|
||||
(( SELECT count(*) AS count
|
||||
FROM preplot_points
|
||||
WHERE preplot_points.line = fl.line AND (preplot_points.point >= s.fsp AND preplot_points.point <= s.lsp OR preplot_points.point >= s.lsp AND preplot_points.point <= s.fsp))) - s.num_preplots AS missing_shots,
|
||||
s.length,
|
||||
s.azimuth,
|
||||
fl.remarks,
|
||||
fl.meta
|
||||
FROM summary s
|
||||
JOIN final_lines fl USING (sequence);
|
||||
|
||||
ALTER TABLE final_lines_summary
|
||||
OWNER TO postgres;
|
||||
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade () AS $outer$
|
||||
DECLARE
|
||||
row RECORD;
|
||||
current_db_version TEXT;
|
||||
BEGIN
|
||||
|
||||
SELECT value->>'db_schema' INTO current_db_version FROM public.info WHERE key = 'version';
|
||||
|
||||
IF current_db_version >= '0.5.3' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Patch already applied';
|
||||
END IF;
|
||||
|
||||
IF current_db_version != '0.5.2' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Invalid database version: ' || current_db_version,
|
||||
HINT='Ensure all previous patches have been applied.';
|
||||
END IF;
|
||||
|
||||
FOR row IN
|
||||
SELECT schema_name FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'survey_%'
|
||||
ORDER BY schema_name
|
||||
LOOP
|
||||
CALL pg_temp.upgrade_survey_schema(row.schema_name);
|
||||
END LOOP;
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CALL pg_temp.upgrade();
|
||||
|
||||
CALL pg_temp.show_notice('Cleaning up');
|
||||
DROP PROCEDURE pg_temp.upgrade_survey_schema (schema_name text);
|
||||
DROP PROCEDURE pg_temp.upgrade ();
|
||||
|
||||
CALL pg_temp.show_notice('Updating db_schema version');
|
||||
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.5.3"}')
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = public.info.value || '{"db_schema": "0.5.3"}' WHERE public.info.key = 'version';
|
||||
|
||||
|
||||
CALL pg_temp.show_notice('All done. You may now run "COMMIT;" to persist the changes');
|
||||
DROP PROCEDURE pg_temp.show_notice (notice text);
|
||||
|
||||
--
|
||||
--NOTE Run `COMMIT;` now if all went well
|
||||
--
|
||||
@@ -0,0 +1,145 @@
|
||||
-- Fix preplot_lines_summary view
|
||||
--
|
||||
-- New schema version: 0.5.4
|
||||
--
|
||||
-- ATTENTION:
|
||||
--
|
||||
-- ENSURE YOU HAVE BACKED UP THE DATABASE BEFORE RUNNING THIS SCRIPT.
|
||||
--
|
||||
--
|
||||
-- NOTE: This upgrade affects all schemas in the database.
|
||||
-- NOTE: Each application starts a transaction, which must be committed
|
||||
-- or rolled back.
|
||||
--
|
||||
-- Fixes upgrade 35 (0.5.2). The original description of 0.5.2 is included
|
||||
-- below for ease of reference:
|
||||
--
|
||||
-- Following introduction of `preplot_saillines` (0.5.0), the incr and
|
||||
-- ntba statuses are stored in a separate table, not in `preplot_lines`
|
||||
-- (TODO: a future upgrade should remove those columns from `preplot_lines`)
|
||||
--
|
||||
-- Now any views referencing `incr` and `ntba` must be updated to point to
|
||||
-- the new location of those attributes.
|
||||
--
|
||||
-- This update fixes #312.
|
||||
--
|
||||
-- To apply, run as the dougal user:
|
||||
--
|
||||
-- psql <<EOF
|
||||
-- \i $THIS_FILE
|
||||
-- COMMIT;
|
||||
-- EOF
|
||||
--
|
||||
-- NOTE: It can be applied multiple times without ill effect.
|
||||
--
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.show_notice (notice text) AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE '%', notice;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade_survey_schema (schema_name text) AS $outer$
|
||||
BEGIN
|
||||
|
||||
RAISE NOTICE 'Updating schema %', schema_name;
|
||||
-- We need to set the search path because some of the trigger
|
||||
-- functions reference other tables in survey schemas assuming
|
||||
-- they are in the search path.
|
||||
EXECUTE format('SET search_path TO %I,public', schema_name);
|
||||
|
||||
CREATE OR REPLACE VIEW preplot_lines_summary
|
||||
AS
|
||||
WITH summary AS (
|
||||
SELECT DISTINCT pp.line,
|
||||
pp.class,
|
||||
first_value(pp.point) OVER w AS p0,
|
||||
last_value(pp.point) OVER w AS p1,
|
||||
count(pp.point) OVER w AS num_points,
|
||||
st_distance(first_value(pp.geometry) OVER w, last_value(pp.geometry) OVER w) AS length,
|
||||
st_azimuth(first_value(pp.geometry) OVER w, last_value(pp.geometry) OVER w) * 180::double precision / pi() AS azimuth0,
|
||||
st_azimuth(last_value(pp.geometry) OVER w, first_value(pp.geometry) OVER w) * 180::double precision / pi() AS azimuth1
|
||||
FROM preplot_points pp
|
||||
WHERE pp.class = 'V'::bpchar
|
||||
WINDOW w AS (PARTITION BY pp.line ORDER BY pp.point ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
|
||||
)
|
||||
SELECT DISTINCT psl.sailline AS line,
|
||||
CASE
|
||||
WHEN psl.incr THEN s.p0
|
||||
ELSE s.p1
|
||||
END AS fsp,
|
||||
CASE
|
||||
WHEN psl.incr THEN s.p1
|
||||
ELSE s.p0
|
||||
END AS lsp,
|
||||
s.num_points,
|
||||
s.length,
|
||||
CASE
|
||||
WHEN psl.incr THEN s.azimuth0
|
||||
ELSE s.azimuth1
|
||||
END AS azimuth,
|
||||
psl.incr,
|
||||
psl.remarks
|
||||
FROM summary s
|
||||
JOIN preplot_saillines psl ON psl.sailline_class = s.class AND s.line = psl.sailline
|
||||
ORDER BY psl.sailline, psl.incr;
|
||||
|
||||
ALTER TABLE preplot_lines_summary
|
||||
OWNER TO postgres;
|
||||
COMMENT ON VIEW preplot_lines_summary
|
||||
IS 'Summarises ''V'' (vessel sailline) preplot lines.';
|
||||
|
||||
|
||||
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade () AS $outer$
|
||||
DECLARE
|
||||
row RECORD;
|
||||
current_db_version TEXT;
|
||||
BEGIN
|
||||
|
||||
SELECT value->>'db_schema' INTO current_db_version FROM public.info WHERE key = 'version';
|
||||
|
||||
IF current_db_version >= '0.5.4' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Patch already applied';
|
||||
END IF;
|
||||
|
||||
IF current_db_version != '0.5.3' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Invalid database version: ' || current_db_version,
|
||||
HINT='Ensure all previous patches have been applied.';
|
||||
END IF;
|
||||
|
||||
FOR row IN
|
||||
SELECT schema_name FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'survey_%'
|
||||
ORDER BY schema_name
|
||||
LOOP
|
||||
CALL pg_temp.upgrade_survey_schema(row.schema_name);
|
||||
END LOOP;
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CALL pg_temp.upgrade();
|
||||
|
||||
CALL pg_temp.show_notice('Cleaning up');
|
||||
DROP PROCEDURE pg_temp.upgrade_survey_schema (schema_name text);
|
||||
DROP PROCEDURE pg_temp.upgrade ();
|
||||
|
||||
CALL pg_temp.show_notice('Updating db_schema version');
|
||||
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.5.4"}')
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = public.info.value || '{"db_schema": "0.5.4"}' WHERE public.info.key = 'version';
|
||||
|
||||
|
||||
CALL pg_temp.show_notice('All done. You may now run "COMMIT;" to persist the changes');
|
||||
DROP PROCEDURE pg_temp.show_notice (notice text);
|
||||
|
||||
--
|
||||
--NOTE Run `COMMIT;` now if all went well
|
||||
--
|
||||
109
etc/db/upgrades/upgrade37-v0.6.0-add-keystore-table.sql
Normal file
109
etc/db/upgrades/upgrade37-v0.6.0-add-keystore-table.sql
Normal file
@@ -0,0 +1,109 @@
|
||||
-- Fix final_lines_summary view
|
||||
--
|
||||
-- New schema version: 0.6.0
|
||||
--
|
||||
-- ATTENTION:
|
||||
--
|
||||
-- ENSURE YOU HAVE BACKED UP THE DATABASE BEFORE RUNNING THIS SCRIPT.
|
||||
--
|
||||
--
|
||||
-- NOTE: This upgrade only affects the `public` schema.
|
||||
-- NOTE: Each application starts a transaction, which must be committed
|
||||
-- or rolled back.
|
||||
--
|
||||
-- This update adds a `keystore` table, intended for storing arbitrary
|
||||
-- key / value pairs which, unlike, the `info` tables, is not meant to
|
||||
-- be directly accessible via the API. Its main purpose as of this writing
|
||||
-- is to store user definitions (see #176, #177, #180).
|
||||
--
|
||||
-- To apply, run as the dougal user:
|
||||
--
|
||||
-- psql <<EOF
|
||||
-- \i $THIS_FILE
|
||||
-- COMMIT;
|
||||
-- EOF
|
||||
--
|
||||
-- NOTE: It can be applied multiple times without ill effect.
|
||||
--
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.show_notice (notice text) AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE '%', notice;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade_database () AS $outer$
|
||||
BEGIN
|
||||
|
||||
RAISE NOTICE 'Updating schema %', 'public';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS keystore (
|
||||
type TEXT NOT NULL, -- A class of data to be stored
|
||||
key TEXT NOT NULL, -- A key that is unique for the class and access type
|
||||
last_modified TIMESTAMP -- To detect update conflicts
|
||||
DEFAULT CURRENT_TIMESTAMP,
|
||||
data jsonb,
|
||||
PRIMARY KEY (type, key) -- Composite primary key
|
||||
);
|
||||
|
||||
-- Create a function to update the last_modified timestamp
|
||||
CREATE OR REPLACE FUNCTION update_last_modified()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.last_modified = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create a trigger that calls the function before each update
|
||||
CREATE OR REPLACE TRIGGER update_keystore_last_modified
|
||||
BEFORE UPDATE ON keystore
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_last_modified();
|
||||
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade () AS $outer$
|
||||
DECLARE
|
||||
row RECORD;
|
||||
current_db_version TEXT;
|
||||
BEGIN
|
||||
|
||||
SELECT value->>'db_schema' INTO current_db_version FROM public.info WHERE key = 'version';
|
||||
|
||||
IF current_db_version >= '0.6.0' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Patch already applied';
|
||||
END IF;
|
||||
|
||||
IF current_db_version != '0.5.3' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Invalid database version: ' || current_db_version,
|
||||
HINT='Ensure all previous patches have been applied.';
|
||||
END IF;
|
||||
|
||||
CALL pg_temp.upgrade_database();
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CALL pg_temp.upgrade();
|
||||
|
||||
CALL pg_temp.show_notice('Cleaning up');
|
||||
DROP PROCEDURE pg_temp.upgrade_database ();
|
||||
DROP PROCEDURE pg_temp.upgrade ();
|
||||
|
||||
CALL pg_temp.show_notice('Updating db_schema version');
|
||||
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.6.0"}')
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = public.info.value || '{"db_schema": "0.6.0"}' WHERE public.info.key = 'version';
|
||||
|
||||
|
||||
CALL pg_temp.show_notice('All done. You may now run "COMMIT;" to persist the changes');
|
||||
DROP PROCEDURE pg_temp.show_notice (notice text);
|
||||
|
||||
--
|
||||
--NOTE Run `COMMIT;` now if all went well
|
||||
--
|
||||
107
etc/db/upgrades/upgrade38-v0.6.1-add-default-user.sql
Normal file
107
etc/db/upgrades/upgrade38-v0.6.1-add-default-user.sql
Normal file
@@ -0,0 +1,107 @@
|
||||
-- Fix final_lines_summary view
|
||||
--
|
||||
-- New schema version: 0.6.1
|
||||
--
|
||||
-- ATTENTION:
|
||||
--
|
||||
-- ENSURE YOU HAVE BACKED UP THE DATABASE BEFORE RUNNING THIS SCRIPT.
|
||||
--
|
||||
--
|
||||
-- NOTE: This upgrade only affects the `public` schema.
|
||||
-- NOTE: Each application starts a transaction, which must be committed
|
||||
-- or rolled back.
|
||||
--
|
||||
-- This update adds a default user to the system (see #176, #177, #180).
|
||||
-- The default user can only be invoked by connecting from localhost.
|
||||
--
|
||||
-- This user has full access to every project via the organisations
|
||||
-- permissions wildcard: `{"*": {read: true, write: true, edit: true}}`
|
||||
-- and can be used to bootstrap the system by creating other users
|
||||
-- and assigning organisational permissions.
|
||||
--
|
||||
-- To apply, run as the dougal user:
|
||||
--
|
||||
-- psql <<EOF
|
||||
-- \i $THIS_FILE
|
||||
-- COMMIT;
|
||||
-- EOF
|
||||
--
|
||||
-- NOTE: It can be applied multiple times without ill effect.
|
||||
--
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.show_notice (notice text) AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE '%', notice;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade_database () AS $outer$
|
||||
BEGIN
|
||||
|
||||
RAISE NOTICE 'Updating schema %', 'public';
|
||||
|
||||
INSERT INTO keystore (type, key, data)
|
||||
VALUES ('user', '6f1e7159-4ca0-4ae4-ab4e-89078166cc10', '
|
||||
{
|
||||
"id": "6f1e7159-4ca0-4ae4-ab4e-89078166cc10",
|
||||
"ip": "127.0.0.0/24",
|
||||
"name": "☠️",
|
||||
"colour": "red",
|
||||
"active": true,
|
||||
"organisations": {
|
||||
"*": {
|
||||
"read": true,
|
||||
"write": true,
|
||||
"edit": true
|
||||
}
|
||||
}
|
||||
}
|
||||
'::jsonb)
|
||||
ON CONFLICT (type, key) DO NOTHING;
|
||||
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE pg_temp.upgrade () AS $outer$
|
||||
DECLARE
|
||||
row RECORD;
|
||||
current_db_version TEXT;
|
||||
BEGIN
|
||||
|
||||
SELECT value->>'db_schema' INTO current_db_version FROM public.info WHERE key = 'version';
|
||||
|
||||
IF current_db_version >= '0.6.1' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Patch already applied';
|
||||
END IF;
|
||||
|
||||
IF current_db_version != '0.6.0' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Invalid database version: ' || current_db_version,
|
||||
HINT='Ensure all previous patches have been applied.';
|
||||
END IF;
|
||||
|
||||
CALL pg_temp.upgrade_database();
|
||||
END;
|
||||
$outer$ LANGUAGE plpgsql;
|
||||
|
||||
CALL pg_temp.upgrade();
|
||||
|
||||
CALL pg_temp.show_notice('Cleaning up');
|
||||
DROP PROCEDURE pg_temp.upgrade_database ();
|
||||
DROP PROCEDURE pg_temp.upgrade ();
|
||||
|
||||
CALL pg_temp.show_notice('Updating db_schema version');
|
||||
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.6.1"}')
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = public.info.value || '{"db_schema": "0.6.1"}' WHERE public.info.key = 'version';
|
||||
|
||||
|
||||
CALL pg_temp.show_notice('All done. You may now run "COMMIT;" to persist the changes');
|
||||
DROP PROCEDURE pg_temp.show_notice (notice text);
|
||||
|
||||
--
|
||||
--NOTE Run `COMMIT;` now if all went well
|
||||
--
|
||||
@@ -45,11 +45,13 @@
|
||||
name: "No fire"
|
||||
id: no_fire
|
||||
check: |
|
||||
const currentShot = currentItem;
|
||||
const gunData = currentItem._("raw_meta.smsrc");
|
||||
(gunData && gunData.guns && gunData.guns.length != gunData.num_active)
|
||||
? `Source ${gunData.src_number}: No fire (${gunData.guns.length - gunData.num_active} guns)`
|
||||
: true;
|
||||
// const currentShot = currentItem;
|
||||
// const gunData = currentItem._("raw_meta.smsrc");
|
||||
// (gunData && gunData.guns && gunData.guns.length != gunData.num_active)
|
||||
// ? `Source ${gunData.src_number}: No fire (${gunData.guns.length - gunData.num_active} guns)`
|
||||
// : true;
|
||||
// Disabled due to changes in Smartsource software. It now returns all guns on every shot, not just active ones.
|
||||
true
|
||||
|
||||
-
|
||||
name: "Pressure errors"
|
||||
|
||||
75
lib/modules/@dougal/organisations/Organisation.js
Normal file
75
lib/modules/@dougal/organisations/Organisation.js
Normal file
@@ -0,0 +1,75 @@
|
||||
|
||||
class Organisation {
|
||||
|
||||
constructor (data) {
|
||||
|
||||
this.read = !!data?.read;
|
||||
this.write = !!data?.write;
|
||||
this.edit = !!data?.edit;
|
||||
|
||||
this.other = {};
|
||||
|
||||
return new Proxy(this, {
|
||||
get (target, prop) {
|
||||
if (prop in target) {
|
||||
return target[prop]
|
||||
} else {
|
||||
return target.other[prop];
|
||||
}
|
||||
},
|
||||
|
||||
set (target, prop, value) {
|
||||
const oldValue = target[prop] !== undefined ? target[prop] : target.other[prop];
|
||||
const newValue = Boolean(value);
|
||||
|
||||
if (["read", "write", "edit"].includes(prop)) {
|
||||
target[prop] = newValue;
|
||||
} else {
|
||||
target.other[prop] = newValue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
return {
|
||||
read: this.read,
|
||||
write: this.write,
|
||||
edit: this.edit,
|
||||
...this.other
|
||||
}
|
||||
}
|
||||
|
||||
toString (replacer, space) {
|
||||
return JSON.stringify(this.toJSON(), replacer, space);
|
||||
}
|
||||
|
||||
/** Limit the operations to only those allowed by `other`
|
||||
*/
|
||||
filter (other) {
|
||||
const filteredOrganisation = new Organisation();
|
||||
|
||||
filteredOrganisation.read = this.read && other.read;
|
||||
filteredOrganisation.write = this.write && other.write;
|
||||
filteredOrganisation.edit = this.edit && other.edit;
|
||||
|
||||
return filteredOrganisation;
|
||||
}
|
||||
|
||||
intersect (other) {
|
||||
return this.filter(other);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Organisation; // CJS export
|
||||
}
|
||||
|
||||
// ESM export
|
||||
if (typeof exports !== 'undefined' && !exports.default) {
|
||||
exports.default = Organisation; // ESM export
|
||||
}
|
||||
225
lib/modules/@dougal/organisations/Organisations.js
Normal file
225
lib/modules/@dougal/organisations/Organisations.js
Normal file
@@ -0,0 +1,225 @@
|
||||
const Organisation = require('./Organisation');
|
||||
|
||||
class Organisations {
|
||||
|
||||
#values = {}
|
||||
|
||||
#overlord
|
||||
|
||||
static entries (orgs) {
|
||||
return orgs.names().map(name => [name, orgs.get(name)]);
|
||||
}
|
||||
|
||||
constructor (data, overlord) {
|
||||
if (data instanceof Organisations) {
|
||||
for (const [name, value] of Organisations.entries(data)) {
|
||||
this.set(name, new Organisation(value));
|
||||
}
|
||||
} else if (data instanceof Object) {
|
||||
for (const [name, value] of Object.entries(data)) {
|
||||
this.set(name, new Organisation(value));
|
||||
}
|
||||
} else if (data instanceof String) {
|
||||
this.set(data, new Organisation());
|
||||
} else if (typeof data !== "undefined") {
|
||||
throw new Error("Invalid constructor argument");
|
||||
}
|
||||
|
||||
if (overlord) {
|
||||
this.#overlord = overlord;
|
||||
}
|
||||
}
|
||||
|
||||
get values () {
|
||||
return this.#values;
|
||||
}
|
||||
|
||||
get length () {
|
||||
return this.names().length;
|
||||
}
|
||||
|
||||
get overlord () {
|
||||
return this.#overlord;
|
||||
}
|
||||
|
||||
set overlord (v) {
|
||||
this.#overlord = new Organisations(v);
|
||||
}
|
||||
|
||||
/** Get the operations for `name`
|
||||
*/
|
||||
get (name) {
|
||||
const key = Object.keys(this.values).find( k => k.toLowerCase() == name.toLowerCase() ) ?? name;
|
||||
return this.values[key];
|
||||
}
|
||||
|
||||
/** Set the operations for `name` to `value`
|
||||
*
|
||||
* If we have an overlord, ensure we cannot:
|
||||
*
|
||||
* 1. Add new organisations which the overlord
|
||||
* is not a member of
|
||||
* 2. Access operations that the overlord is not
|
||||
* allowed to access
|
||||
*/
|
||||
set (name, value) {
|
||||
name = String(name).trim();
|
||||
const key = Object.keys(this.values).find( k => k.toLowerCase() == name.toLowerCase() ) ?? name;
|
||||
const org = new Organisation(value);
|
||||
|
||||
if (this.overlord) {
|
||||
const parent = this.overlord.get(key) ?? this.overlord.get("*");
|
||||
if (parent) {
|
||||
this.values[key] = parent.filter(org);
|
||||
}
|
||||
} else {
|
||||
this.values[key] = new Organisation(value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Enable the operation `op` in all organisations
|
||||
*/
|
||||
enableOperation (op) {
|
||||
if (this.overlord) {
|
||||
Object.keys(this.#values)
|
||||
.filter( key => (this.overlord.get(key) ?? this.overlord.get("*"))?.[op] )
|
||||
.forEach( key => this.#values[key][op] = true );
|
||||
} else {
|
||||
Object.values(this.#values).forEach( org => org[op] = true );
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Disable the operation `op` in all organisations
|
||||
*/
|
||||
disableOperation (op) {
|
||||
Object.values(this.#values).forEach( org => org[op] = false );
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Create a new organisation object limited by the caller's rights
|
||||
*
|
||||
* The spawned Organisations instance will have the same organisations
|
||||
* and rights as the caller minus the applied `mask`. With the default
|
||||
* mask, the spawned object will inherit all rights except for `edit`
|
||||
* rights.
|
||||
*
|
||||
* The "*" organisation must be explicitly assigned. It is not inherited.
|
||||
*/
|
||||
spawn (mask = {read: true, write: true, edit: false}) {
|
||||
|
||||
const parent = new Organisations();
|
||||
const wildcard = this.get("*").edit; // If true, we can spawn everywhere
|
||||
|
||||
this.entries().forEach( ([k, v]) => {
|
||||
// if (k != "*") { // This organisation is not inherited
|
||||
if (v.edit || wildcard) { // We have the right to spawn in this organisation
|
||||
const o = new Organisation({
|
||||
read: v.read && mask.read,
|
||||
write: v.write && mask.write,
|
||||
edit: v.edit && mask.edit
|
||||
});
|
||||
parent.set(k, o);
|
||||
}
|
||||
// }
|
||||
});
|
||||
|
||||
return new Organisations({}, parent);
|
||||
}
|
||||
|
||||
remove (name) {
|
||||
const key = Object.keys(this.values).find( k => k.toLowerCase() == name.toLowerCase() ) ?? name;
|
||||
delete this.values[key];
|
||||
}
|
||||
|
||||
/** Return the list of organisation names
|
||||
*/
|
||||
names () {
|
||||
return Object.keys(this.values);
|
||||
}
|
||||
|
||||
/** Same as this.get(name)
|
||||
*/
|
||||
value (name) {
|
||||
return this.values[name];
|
||||
}
|
||||
|
||||
/** Same as Object.prototype.entries
|
||||
*/
|
||||
entries () {
|
||||
return this.names().map( name => [ name, this.value(name) ] );
|
||||
}
|
||||
|
||||
/** Return true if the named organisation is present
|
||||
*/
|
||||
has (name) {
|
||||
return Boolean(this.value(name));
|
||||
}
|
||||
|
||||
/** Return only those of our organisations
|
||||
* and operations present in `other`
|
||||
*/
|
||||
filter (other) {
|
||||
const filteredOrganisations = new Organisations();
|
||||
|
||||
const wildcard = other.value("*");
|
||||
|
||||
for (const [name, org] of this.entries()) {
|
||||
const ownOrg = other.value(name) ?? wildcard;
|
||||
if (ownOrg) {
|
||||
filteredOrganisations.set(name, org.filter(ownOrg))
|
||||
}
|
||||
}
|
||||
|
||||
return filteredOrganisations;
|
||||
}
|
||||
|
||||
/** Return only those organisations
|
||||
* that have access to the required
|
||||
* operation
|
||||
*/
|
||||
accessToOperation (op) {
|
||||
const filteredOrganisations = new Organisations();
|
||||
|
||||
for (const [name, org] of this.entries()) {
|
||||
if (org[op]) {
|
||||
filteredOrganisations.set(name, org);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredOrganisations;
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
const obj = {};
|
||||
for (const key in this.values) {
|
||||
obj[key] = this.values[key].toJSON();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
toString (replacer, space) {
|
||||
return JSON.stringify(this.toJSON(), replacer, space);
|
||||
}
|
||||
|
||||
*[Symbol.iterator] () {
|
||||
for (const [name, operations] of this.entries()) {
|
||||
yield {name, operations};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Organisations; // CJS export
|
||||
}
|
||||
|
||||
// ESM export
|
||||
if (typeof exports !== 'undefined' && !exports.default) {
|
||||
exports.default = Organisations; // ESM export
|
||||
}
|
||||
5
lib/modules/@dougal/organisations/index.js
Normal file
5
lib/modules/@dougal/organisations/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
Organisation: require('./Organisation'),
|
||||
Organisations: require('./Organisations')
|
||||
}
|
||||
12
lib/modules/@dougal/organisations/package.json
Normal file
12
lib/modules/@dougal/organisations/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@dougal/organisations",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": ""
|
||||
}
|
||||
364
lib/modules/@dougal/user/User.js
Normal file
364
lib/modules/@dougal/user/User.js
Normal file
@@ -0,0 +1,364 @@
|
||||
const EventEmitter = require('events');
|
||||
const { Organisations } = require('@dougal/organisations');
|
||||
|
||||
function randomUUID () {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
class User extends EventEmitter {
|
||||
|
||||
// Valid field names
|
||||
static fields = [ "ip", "host", "name", "email", "description", "colour", "active", "organisations", "meta" ]
|
||||
|
||||
static validUUID (str) {
|
||||
const uuidv4Rx = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidv4Rx.test(str);
|
||||
}
|
||||
|
||||
static validIPv4 (str) {
|
||||
const ipv4Rx = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
|
||||
return ipv4Rx.test(str);
|
||||
}
|
||||
|
||||
static validIPv6 (str) {
|
||||
const ipv6Rx = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:((?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?))|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?))))$/;
|
||||
return ipv6Rx.test(str);
|
||||
}
|
||||
|
||||
static validHostname (str) {
|
||||
const hostnameRx = /^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,}$/;
|
||||
return hostnameRx.test(str);
|
||||
}
|
||||
|
||||
#setString (k, v) {
|
||||
if (typeof v === "undefined") {
|
||||
this.values[k] = v;
|
||||
} else {
|
||||
this.values[k] = String(v).trim();
|
||||
}
|
||||
this.emit("changed", k, v);
|
||||
this.#updateTimestamp();
|
||||
}
|
||||
|
||||
#updateTimestamp (v) {
|
||||
if (typeof v === "undefined") {
|
||||
this.#timestamp = (new Date()).valueOf();
|
||||
} else {
|
||||
this.#timestamp = (new Date(v)).valueOf();
|
||||
}
|
||||
this.emit("last_modified", this.#timestamp);
|
||||
}
|
||||
|
||||
// Create a new instance of `other`, where `other` is
|
||||
// an instance of User or of a derived class
|
||||
#clone (other = this) {
|
||||
const clone = new this.constructor();
|
||||
Object.assign(clone.values, other.values);
|
||||
clone.organisations = new Organisations(other.organisations);
|
||||
return clone;
|
||||
}
|
||||
|
||||
values = {}
|
||||
|
||||
#timestamp
|
||||
|
||||
constructor (data) {
|
||||
super();
|
||||
|
||||
User.fields.forEach( f => this[f] = data?.[f] );
|
||||
this.values.id = data?.id ?? randomUUID();
|
||||
this.values.active = !!this.active;
|
||||
this.values.hash = data?.hash;
|
||||
this.values.password = data?.password;
|
||||
this.values.organisations = new Organisations(data?.organisations);
|
||||
this.#updateTimestamp(data?.last_modified);
|
||||
}
|
||||
|
||||
/*
|
||||
* Getters
|
||||
*/
|
||||
|
||||
get id () { return this.values.id }
|
||||
|
||||
get ip () { return this.values.ip }
|
||||
|
||||
get host () { return this.values.host }
|
||||
|
||||
get name () { return this.values.name }
|
||||
|
||||
get email () { return this.values.email }
|
||||
|
||||
get description () { return this.values.description }
|
||||
|
||||
get colour () { return this.values.colour }
|
||||
|
||||
get active () { return this.values.active }
|
||||
|
||||
get organisations () { return this.values.organisations }
|
||||
|
||||
get password () { return this.values.password }
|
||||
|
||||
get timestamp () { return new Date(this.#timestamp) }
|
||||
|
||||
/*
|
||||
* Setters
|
||||
*/
|
||||
|
||||
set id (v) {
|
||||
if (typeof v === "undefined") {
|
||||
this.values.id = randomUUID();
|
||||
} else if (User.validUUID(v)) {
|
||||
this.values.id = v;
|
||||
} else {
|
||||
throw new Error("Invalid ID format (must be UUIDv4)");
|
||||
}
|
||||
this.emit("changed", "id", this.values.id);
|
||||
this.#updateTimestamp();
|
||||
}
|
||||
|
||||
set ip (v) {
|
||||
if (User.validIPv4(v) || User.validIPv6(v) || typeof v === "undefined") {
|
||||
this.values.ip = v;
|
||||
} else {
|
||||
throw new Error("Invalid IP address or subnet");
|
||||
}
|
||||
this.emit("changed", "ip", this.values.ip);
|
||||
this.#updateTimestamp();
|
||||
}
|
||||
|
||||
set host (v) {
|
||||
if (User.validHostname(v) || typeof v === "undefined") {
|
||||
this.values.host = v;
|
||||
} else {
|
||||
throw new Error("Invalid hostname");
|
||||
}
|
||||
this.emit("changed", "host", this.values.host);
|
||||
this.#updateTimestamp();
|
||||
}
|
||||
|
||||
set name (v) {
|
||||
this.#setString("name", v);
|
||||
}
|
||||
|
||||
set email (v) {
|
||||
// TODO should validate, buy hey!
|
||||
this.#setString("email", v);
|
||||
}
|
||||
|
||||
set description (v) {
|
||||
this.#setString("description", v);
|
||||
}
|
||||
|
||||
set colour (v) {
|
||||
this.#setString("colour", v);
|
||||
}
|
||||
|
||||
set active (v) {
|
||||
this.values.active = !!v;
|
||||
this.emit("changed", "active", this.values.active);
|
||||
this.#updateTimestamp();
|
||||
}
|
||||
|
||||
set organisations (v) {
|
||||
this.values.organisations = new Organisations(v);
|
||||
this.emit("changed", "organisations", this.values.organisations);
|
||||
this.#updateTimestamp();
|
||||
}
|
||||
|
||||
set password (v) {
|
||||
this.values.password = v;
|
||||
this.emit("changed", "password", this.values.password);
|
||||
this.#updateTimestamp();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Validation methods
|
||||
*/
|
||||
|
||||
get errors () {
|
||||
let err = [];
|
||||
|
||||
if (!this.id) err.push("ERR_NO_ID");
|
||||
if (!this.name) err.push("ERR_NO_NAME");
|
||||
if (!this.organisations.length) err.push("ERR_NO_ORG");
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
get isValid () {
|
||||
return this.errors.length == 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Filtering methods
|
||||
*/
|
||||
|
||||
filter (other) {
|
||||
// const filteredUser = new User(this);
|
||||
const filteredUser = this.#clone();
|
||||
filteredUser.organisations = this.organisations.filter(other.organisations);
|
||||
return filteredUser;
|
||||
}
|
||||
|
||||
/** Return users that are visible to me.
|
||||
*
|
||||
* These are users with which at leas one common organisation
|
||||
* with read, write or delete access to.
|
||||
*
|
||||
* If we are wildcarded ("*"), we see everyone.
|
||||
*
|
||||
* If a peer is wildcarded, they can be seen by everone.
|
||||
*/
|
||||
peers (list) {
|
||||
if (this.organisations.value("*")) {
|
||||
return list;
|
||||
} else {
|
||||
return list.filter( user => this.canRead(user) );
|
||||
// return list.filter( user =>
|
||||
// user.organisations.value("*") ||
|
||||
// user.organisations.filter(this.organisations).length > 0
|
||||
// this.organisations.filter(user.organisations).length > 0
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
/** Return users that I can edit
|
||||
*
|
||||
* These users must belong to an organisation
|
||||
* over which I have edit rights.
|
||||
*
|
||||
* If we are edit wildcarded, we can edit everyone.
|
||||
*/
|
||||
editablePeers (list) {
|
||||
const editableOrgs = this.organisations.accessToOperation("edit");
|
||||
if (editableOrgs.value("*")) {
|
||||
return list;
|
||||
} else {
|
||||
return list.filter( user => this.canEdit(user) );
|
||||
// editableOrgs.filter(user.organisations).length > 0
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* General methods
|
||||
*/
|
||||
|
||||
/** Return `true` if we are `other`
|
||||
*/
|
||||
is (other) {
|
||||
return this.id == other.id;
|
||||
}
|
||||
|
||||
canDo (operation, other) {
|
||||
if (this.organisations.get('*')?.[operation])
|
||||
return true;
|
||||
|
||||
if (other instanceof User) {
|
||||
return other.organisations.names().some(name => this.organisations.get(name)?.[operation]);
|
||||
} else if (other instanceof Organisations) {
|
||||
return other.accessToOperation(operation).names().some(name => this.organisations.get(name)?.[operation]);
|
||||
} else if (other?.organisations) {
|
||||
return this.canDo(operation, new Organisations(other.organisations));
|
||||
} else if (other instanceof Object) {
|
||||
return this.canDo(operation, new Organisations(other));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
canRead (other) {
|
||||
return this.canDo("read", other);
|
||||
}
|
||||
|
||||
canWrite (other) {
|
||||
return this.canDo("write", other);
|
||||
}
|
||||
|
||||
canEdit (other) {
|
||||
return this.canDo("edit", other);
|
||||
}
|
||||
|
||||
/** Perform an edit on another user
|
||||
*
|
||||
* Syntax: user.edit(other).to(another);
|
||||
*
|
||||
* Applies to `other` the changes described in `another`
|
||||
* that are permitted to `user`. The argument `another`
|
||||
* must be a plain object (not a `User` instance) with
|
||||
* only the properties that are to be changed.
|
||||
*
|
||||
* NOTE: Organisations are not merged, they are overwritten
|
||||
* and then filtered to ensure that the edited user does not
|
||||
* gain more privileges than those granted to the editing
|
||||
* user.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // This causes user test77 to set user x23 to
|
||||
* // inactive
|
||||
* test77.edit(x23).to({active: false})
|
||||
*/
|
||||
edit (other) {
|
||||
if (this.canEdit(other)) {
|
||||
return {
|
||||
to: (another) => {
|
||||
const newUser = Object.assign(this.#clone(other), another);
|
||||
return newUser.filter(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not fail or throw but return undefined
|
||||
}
|
||||
|
||||
/** Create a new user similar to us except it doesn't have `edit` rights
|
||||
* by default
|
||||
*/
|
||||
spawn (init = {}, mask = {read: true, write: true, edit: false}) {
|
||||
// const user = new User(init);
|
||||
const user = this.#clone(init);
|
||||
user.organisations = this.organisations.accessToOperation("edit").disableOperation("edit");
|
||||
user.organisations.overlord = this.organisations;
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Conversion and presentation methods
|
||||
*/
|
||||
|
||||
toJSON () {
|
||||
return {
|
||||
id: this.id,
|
||||
ip: this.ip,
|
||||
host: this.host,
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
description: this.description,
|
||||
colour: this.colour,
|
||||
active: this.active,
|
||||
organisations: this.organisations.toJSON(),
|
||||
password: this.password
|
||||
}
|
||||
}
|
||||
|
||||
toString (replacer, space) {
|
||||
return JSON.stringify(this.toJSON(), replacer, space);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = User; // CJS export
|
||||
}
|
||||
|
||||
// ESM export
|
||||
if (typeof exports !== 'undefined' && !exports.default) {
|
||||
exports.default = User; // ESM export
|
||||
}
|
||||
4
lib/modules/@dougal/user/index.js
Normal file
4
lib/modules/@dougal/user/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
module.exports = {
|
||||
User: require('./User')
|
||||
}
|
||||
24
lib/modules/@dougal/user/package-lock.json
generated
Normal file
24
lib/modules/@dougal/user/package-lock.json
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@dougal/user",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@dougal/user",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@dougal/organisations": "file:../organisations"
|
||||
}
|
||||
},
|
||||
"../organisations": {
|
||||
"version": "1.0.0",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@dougal/organisations": {
|
||||
"resolved": "../organisations",
|
||||
"link": true
|
||||
}
|
||||
}
|
||||
}
|
||||
15
lib/modules/@dougal/user/package.json
Normal file
15
lib/modules/@dougal/user/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@dougal/user",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@dougal/organisations": "file:../organisations"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ module.exports = {
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-logical-assignment-operators'
|
||||
'@babel/plugin-proposal-logical-assignment-operators',
|
||||
'@babel/plugin-transform-private-methods'
|
||||
]
|
||||
}
|
||||
|
||||
664
lib/www/client/source/package-lock.json
generated
664
lib/www/client/source/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,12 @@
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dougal/organisations": "file:../../../modules/@dougal/organisations",
|
||||
"@dougal/user": "file:../../../modules/@dougal/user",
|
||||
"@mdi/font": "^7.2.96",
|
||||
"buffer": "^6.0.3",
|
||||
"core-js": "^3.6.5",
|
||||
"csv-parse": "^5.5.2",
|
||||
"d3": "^7.0.1",
|
||||
"jwt-decode": "^3.0.0",
|
||||
"leaflet": "^1.7.1",
|
||||
@@ -24,10 +28,12 @@
|
||||
"vue-debounce": "^2.6.0",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuetify": "^2.5.0",
|
||||
"vuex": "^3.6.2"
|
||||
"vuex": "^3.6.2",
|
||||
"yaml": "^2.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.14.5",
|
||||
"@babel/plugin-transform-private-methods": "^7.27.1",
|
||||
"@vue/cli-plugin-babel": "^5.0.8",
|
||||
"@vue/cli-plugin-router": "^5.0.8",
|
||||
"@vue/cli-plugin-vuex": "^5.0.8",
|
||||
|
||||
@@ -82,6 +82,8 @@ export default {
|
||||
if (event.channel == "project" && event.payload?.schema == "public") {
|
||||
// Projects changed in some way or another
|
||||
await this.refreshProjects();
|
||||
} else if (event.channel == ".jwt" && event.payload?.token) {
|
||||
await this.setCredentials({token: event.payload?.token});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="dialogOpen"
|
||||
@input="(e) => $emit('input', e)"
|
||||
max-width="600"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-if="adminaccess()"
|
||||
title="Create a new project from scratch. Generally, it's preferable to clone an existing project (right-click → ‘Clone’)"
|
||||
small
|
||||
outlined
|
||||
color="warning"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<span>Create new project</span>
|
||||
<v-icon right small>mdi-file-document-plus-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<dougal-project-settings-name-id-geodetics
|
||||
:value="newProjectDetails"
|
||||
@input="save"
|
||||
@close="dialogOpen = false"
|
||||
>
|
||||
</dougal-project-settings-name-id-geodetics>
|
||||
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import DougalProjectSettingsNameIdGeodetics from '@/components/project-settings/name-id-geodetics'
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: 'DougalAppBarExtensionProjectList',
|
||||
|
||||
components: {
|
||||
DougalProjectSettingsNameIdGeodetics
|
||||
},
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
dialogOpen: false,
|
||||
newProjectDetails: {
|
||||
name: null,
|
||||
id: null,
|
||||
epsg: null
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
async save (data) {
|
||||
this.dialogOpen = false;
|
||||
data.archived = true; // Make the project inactive to start with
|
||||
console.log("POST the new project data");
|
||||
console.log(data);
|
||||
|
||||
const init = {
|
||||
method: "POST",
|
||||
body: data
|
||||
};
|
||||
const cb = (err, res) => {
|
||||
if (!err && res) {
|
||||
console.log(res);
|
||||
if (res.status == "201") {
|
||||
// Redirect to new project settings page
|
||||
const settingsUrl = `/projects/${data.id.toLowerCase()}/configuration`;
|
||||
this.$router.push(settingsUrl);
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
await this.api(["/project", init, cb]);
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,14 +1,29 @@
|
||||
<template>
|
||||
<v-tabs :value="tab" show-arrows>
|
||||
<v-tabs :value="tab" show-arrows v-if="page != 'configuration'">
|
||||
<v-tab v-for="tab, index in tabs" :key="index" link :to="tabLink(tab.href)" v-text="tab.text"></v-tab>
|
||||
<template v-if="adminaccess()">
|
||||
<v-spacer></v-spacer>
|
||||
<v-tab :to="tabLink('configuration')" class="orange--text darken-3" title="Edit project settings"><v-icon small left color="orange darken-3">mdi-cog-outline</v-icon> Settings</v-tab>
|
||||
</template>
|
||||
</v-tabs>
|
||||
<v-tabs optional :value="0" show-arrows align-with-title v-else>
|
||||
<v-tab>Project settings</v-tab>
|
||||
<v-spacer></v-spacer>
|
||||
<v-tab :to="tabLink('summary')">Go to project</v-tab>
|
||||
</v-tabs>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: 'DougalAppBarExtensionProject',
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<v-row
|
||||
dense
|
||||
no-gutters
|
||||
align="center"
|
||||
>
|
||||
|
||||
<v-col cols="1">
|
||||
<slot name="prepend"></slot>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-chip outlined label small :color="colour || getHSLColourFor(key)">{{name}}</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
dense
|
||||
label="Column"
|
||||
type="number"
|
||||
min="0"
|
||||
clearable
|
||||
:value="value.column"
|
||||
@input="$emit('input', {...value, column: Number($event)})"
|
||||
>
|
||||
<template v-slot:append-outer>
|
||||
<dougal-field-content-dialog
|
||||
:readonly="readonly"
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
></dougal-field-content-dialog>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="1">
|
||||
<slot name="append"></slot>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { parse } from 'csv-parse/sync'
|
||||
import { getHSLColourFor } from '@/lib/hsl'
|
||||
import DougalFieldContentDialog from '../fields/field-content-dialog'
|
||||
|
||||
export default {
|
||||
name: "DougalDelimitedStringDecoderField",
|
||||
|
||||
components: {
|
||||
//DougalFixedStringDecoderField,
|
||||
DougalFieldContentDialog
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
name: String,
|
||||
colour: String,
|
||||
readonly: Boolean,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
getHSLColourFor: getHSLColourFor.bind(this),
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<v-card flat elevation="0">
|
||||
<v-card-title v-if="title">{{ title }}</v-card-title>
|
||||
<v-card-subtitle v-if="subtitle">{{ subtitle }}</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
|
||||
<v-container>
|
||||
|
||||
<dougal-delimited-string-decoder-field v-for="(field, key) in fields" :key="key"
|
||||
:colour="getHSLColourFor(key)"
|
||||
:readonly="readonly"
|
||||
:name="key"
|
||||
:value="fields[key]"
|
||||
@input="$emit('update:fields', {...fields, [key]: $event})"
|
||||
>
|
||||
<template v-slot:append v-if="editableFieldList && !readonly">
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Remove this property"
|
||||
>
|
||||
<v-icon
|
||||
color="error"
|
||||
@click="removeField(key)"
|
||||
>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dougal-delimited-string-decoder-field>
|
||||
<v-row dense no-gutters v-if="editableFieldList && !readonly">
|
||||
<v-col cols=6 offset=1>
|
||||
<v-text-field
|
||||
label="Add new field"
|
||||
hint="Enter the name of a new field"
|
||||
:error-messages="fieldNameErrors"
|
||||
v-model="fieldName"
|
||||
append-outer-icon="mdi-plus-circle"
|
||||
@keydown.enter.prevent="addField"
|
||||
>
|
||||
<template v-slot:append-outer>
|
||||
<v-icon
|
||||
color="primary"
|
||||
:disabled="fieldName && !!fieldNameErrors"
|
||||
@click="addField"
|
||||
>mdi-plus</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-combobox
|
||||
label="Field delimiter"
|
||||
hint="How are the fields separated from each other?"
|
||||
:items="delimiters"
|
||||
v-model="delimiter_"
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
label="Skip lines"
|
||||
hint="This lets you to skip file headers if present"
|
||||
type="number"
|
||||
min="0"
|
||||
:value.number="numberedLines"
|
||||
@input="$emit('update:numbered-lines', Number($event))"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-checkbox
|
||||
v-ripple
|
||||
label="First non-skipped line are field names"
|
||||
:value="headerRow"
|
||||
@change="$emit('update:header-row', $event)"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-simple-table dense>
|
||||
<template v-slot:default>
|
||||
<colgroup v-if="showLineNumbers">
|
||||
<col class="line_no"/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="line_no">
|
||||
<v-simple-checkbox
|
||||
off-icon="mdi-format-list-numbered"
|
||||
title="Show line numbers"
|
||||
v-model="showLineNumbers"
|
||||
>
|
||||
</v-simple-checkbox>
|
||||
</th>
|
||||
<th v-for="(header, idx) in headers" :key="idx"
|
||||
:style="`color:${header.colour};`"
|
||||
>
|
||||
<v-select
|
||||
dense
|
||||
clearable
|
||||
:items="fieldsAvailableFor(idx)"
|
||||
:value="header.fieldName"
|
||||
@input="fieldSelected(idx, $event)"
|
||||
>
|
||||
</v-select>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="line_no">
|
||||
<small v-if="showLineNumbers && headers.length">Line no.</small>
|
||||
</th>
|
||||
<th v-for="(header, idx) in headers" :key="idx"
|
||||
:style="`color:${header.colour};`"
|
||||
>
|
||||
{{ header.text }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, ridx) in rows" :key="ridx">
|
||||
<td class="line_no"">
|
||||
<small v-if="showLineNumbers">
|
||||
{{ ridx + (typeof numberedLines == "number" ? numberedLines : 0)+1 }}
|
||||
</small>
|
||||
</td>
|
||||
<td v-for="(cell, cidx) in row" :key="cidx"
|
||||
:style="`background-color:${cell.colour};`"
|
||||
>
|
||||
{{ cell.text }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
|
||||
|
||||
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/*.v-data-table table tbody tr td*/
|
||||
th {
|
||||
border: 1px solid hsl(0, 0%, 33.3%);
|
||||
}
|
||||
|
||||
td {
|
||||
border-inline: 1px solid hsl(0, 0%, 33.3%);
|
||||
}
|
||||
|
||||
.line_no {
|
||||
text-align: right;
|
||||
width: 4ex;
|
||||
border: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { parse } from 'csv-parse/sync'
|
||||
import { getHSLColourFor } from '@/lib/hsl'
|
||||
import truncateText from '@/lib/truncate-text'
|
||||
import DougalDelimitedStringDecoderField from './delimited-string-decoder-field'
|
||||
|
||||
export default {
|
||||
name: "DougalDelimitedStringDecoder",
|
||||
|
||||
components: {
|
||||
DougalDelimitedStringDecoderField
|
||||
},
|
||||
|
||||
props: {
|
||||
text: String,
|
||||
fields: Object,
|
||||
delimiter: String,
|
||||
headerRow: { type: [ Boolean, Number ], default: false},
|
||||
numberedLines: [ Boolean, Number ],
|
||||
maxHeight: String,
|
||||
editableFieldList: { type: Boolean, default: true },
|
||||
readonly: Boolean,
|
||||
title: String,
|
||||
subtitle: String
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
//< The reason for not using this.text directly is that at some point
|
||||
//< we might extend this component to allow editing the sample text.
|
||||
text_: "",
|
||||
//< The name of a new field to add.
|
||||
fieldName: "",
|
||||
showLineNumbers: null,
|
||||
delimiters: [
|
||||
{ text: "Comma (,)", value: "," },
|
||||
{ text: "Tabulator (⇥)", value: "\x09" },
|
||||
{ text: "Semicolon (;)", value: ";" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
/** The index of the last column.
|
||||
*
|
||||
* This will be the higher of the number of columns available
|
||||
* in the sample text or the highest column number defined in
|
||||
* this.fields.
|
||||
*
|
||||
* NOTE: May return NaN
|
||||
*/
|
||||
numberOfColumns () {
|
||||
const lastIndex = Object.values(this.fields)
|
||||
.reduce( (acc, cur) => Math.max(acc, cur.column), this.cells[0]?.length-1);
|
||||
return isNaN(lastIndex) ? 0 : (lastIndex + 1);
|
||||
},
|
||||
|
||||
cells () {
|
||||
return parse(this.text_, {delimiter: this.delimiter, trim: true});
|
||||
},
|
||||
|
||||
headers () {
|
||||
|
||||
const headerNames = typeof this.headerRow == "number"
|
||||
? this.cells[this.headerRow]
|
||||
: this.headerRow === true
|
||||
? this.cells[0]
|
||||
: Array.from(this.cells[0] ?? [], (_, ι) => `Column ${ι}`);
|
||||
|
||||
return headerNames?.map((c, ι) => {
|
||||
const fieldName = Object.keys(this.fields).find(i => this.fields[i].column == ι);
|
||||
const field = this.fields[fieldName] ?? {}
|
||||
const colour = this.headerRow === false
|
||||
? this.getHSLColourFor(ι*10)
|
||||
: this.getHSLColourFor(c);
|
||||
|
||||
return {
|
||||
text: c,
|
||||
colour: this.getHSLColourFor(c),
|
||||
fieldName,
|
||||
field
|
||||
} ?? {}
|
||||
}) ?? [];
|
||||
},
|
||||
|
||||
rows () {
|
||||
// NOTE It doesn't matter if headerRow is boolean, it works just the same.
|
||||
return [...this.cells].slice(this.headerRow).map(r =>
|
||||
r.map( (c, ι) => ({
|
||||
text: truncateText(c),
|
||||
colour: this.headers.length
|
||||
? this.getHSLColourFor(this.headers[ι]?.text, 0.2)
|
||||
: this.getHSLColourFor(ι*10, 0.2)
|
||||
})));
|
||||
},
|
||||
|
||||
fieldNameErrors () {
|
||||
return Object.keys(this.fields).includes(this.fieldName)
|
||||
? "A field with this name already exists"
|
||||
: null;
|
||||
},
|
||||
|
||||
delimiter_: {
|
||||
get () {
|
||||
return this.delimiters.find(i => i.value == this.delimiter) ?? this.delimiter;
|
||||
},
|
||||
|
||||
set (v) {
|
||||
this.$emit("update:delimiter", typeof v == "object" ? v.value : v);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
text () {
|
||||
if (this.text != this.text_) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
|
||||
numberedLines (cur, prev) {
|
||||
if (cur != prev) {
|
||||
this.showLineNumbers = typeof cur == "number" || cur;
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
|
||||
|
||||
fieldsAvailableFor (idx) {
|
||||
return Object.keys(this.fields).filter( i =>
|
||||
this.fields[i].column === idx || this.fields[i].column === null) ?? [];
|
||||
},
|
||||
|
||||
fieldSelected (col, key) {
|
||||
|
||||
const fields = {};
|
||||
for (const k in this.fields) {
|
||||
const field = {...this.fields[k]};
|
||||
if (k === key) {
|
||||
field.column = col
|
||||
} else {
|
||||
if (field.column === col) {
|
||||
field.column = null;
|
||||
}
|
||||
}
|
||||
fields[k] = field;
|
||||
}
|
||||
|
||||
this.$emit("update:fields", fields);
|
||||
|
||||
},
|
||||
|
||||
addField () {
|
||||
if (!this.fieldNameErrors) {
|
||||
this.$emit("update:fields", {
|
||||
...this.fields,
|
||||
[this.fieldName]: { column: null }
|
||||
});
|
||||
this.fieldName = "";
|
||||
}
|
||||
},
|
||||
|
||||
removeField (key) {
|
||||
const fields = {...this.fields};
|
||||
delete fields[key];
|
||||
this.$emit("update:fields", fields);
|
||||
},
|
||||
|
||||
getHSLColourFor: getHSLColourFor.bind(this),
|
||||
|
||||
numberLine (number, line) {
|
||||
return `<span class="line-number">${number}</span>${line}`;
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.text_ = this.text.replaceAll("\r", "");
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<v-row dense no-gutters>
|
||||
|
||||
<v-col cols="1">
|
||||
<slot name="prepend"></slot>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-chip outlined label small :color="colour">{{name}}</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="From"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="value.offset"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="Length"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="value.length"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<dougal-field-content-dialog
|
||||
:readonly="readonly"
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
></dougal-field-content-dialog>
|
||||
|
||||
</v-col>
|
||||
|
||||
<v-col cols="1">
|
||||
<slot name="append"></slot>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input >>> .chunk {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import DougalFieldContentDialog from '../fields/field-content-dialog'
|
||||
|
||||
export default {
|
||||
name: "DougalFixedStringDecoderField",
|
||||
|
||||
components: {
|
||||
DougalFieldContentDialog
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
name: String,
|
||||
colour: String,
|
||||
readonly: Boolean,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
name_: "",
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
name () {
|
||||
if (this.name != this.name_) {
|
||||
this.name_ = this.name;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
addField () {
|
||||
if (!this.fieldNameErrors) {
|
||||
this.$emit("update:fields", {
|
||||
...this.fields,
|
||||
[this.fieldName]: { offset: 0, length: 0 }
|
||||
});
|
||||
this.fieldName = "";
|
||||
}
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.text_ = this.text;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,486 @@
|
||||
<template>
|
||||
<v-card flat elevation="0">
|
||||
<v-card-title v-if="title">{{ title }}</v-card-title>
|
||||
<v-card-subtitle v-if="subtitle">{{ subtitle }}</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<div v-if="isMultiline"
|
||||
class="multiline mb-5"
|
||||
:style="multilineElementStyle"
|
||||
v-html="html"
|
||||
>
|
||||
</div>
|
||||
<v-input v-else
|
||||
class="v-text-field"
|
||||
:hint="hint"
|
||||
persistent-hint
|
||||
v-model="text_"
|
||||
>
|
||||
<label
|
||||
class="v-label"
|
||||
:class="[ $vuetify.theme.isDark && 'theme--dark', text_ && text_.length && 'v-label--active' ]"
|
||||
style="left: 0px; right: auto; position: absolute;"
|
||||
>{{ label }}</label>
|
||||
<div class="input"
|
||||
:class="isMultiline ? 'multiline' : ''"
|
||||
v-html="html"
|
||||
>
|
||||
</div>
|
||||
</v-input>
|
||||
|
||||
<v-container>
|
||||
|
||||
<!-- Variable fields -->
|
||||
|
||||
<v-row no-gutters class="mb-2">
|
||||
<h4>Variable fields</h4>
|
||||
</v-row>
|
||||
|
||||
<dougal-fixed-string-decoder-field v-for="(field, key) in fields" :key="key"
|
||||
v-model="fields[key]"
|
||||
:name="key"
|
||||
:colour="getHSLColourFor(key)"
|
||||
:readonly="readonly"
|
||||
>
|
||||
<template v-slot:append v-if="editableFieldList && !readonly">
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Remove this property"
|
||||
>
|
||||
<v-icon
|
||||
color="error"
|
||||
@click="removeField(key)"
|
||||
>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dougal-fixed-string-decoder-field>
|
||||
|
||||
<v-row dense no-gutters v-if="editableFieldList && !readonly">
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
label="Add new field"
|
||||
hint="Enter the name of a new field"
|
||||
:error-messages="fieldNameErrors"
|
||||
v-model="fieldName"
|
||||
append-outer-icon="mdi-plus-circle"
|
||||
@keydown.enter.prevent="addField"
|
||||
>
|
||||
<template v-slot:append-outer>
|
||||
<v-icon
|
||||
color="primary"
|
||||
:disabled="fieldName && !!fieldNameErrors"
|
||||
@click="addField"
|
||||
>mdi-plus</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Fixed text strings -->
|
||||
|
||||
<v-row no-gutters class="mt-2 mb-2">
|
||||
<h4>Fixed strings</h4>
|
||||
</v-row>
|
||||
|
||||
<dougal-fixed-string-text v-for="(item, idx) in fixed" :key="idx"
|
||||
v-model="fixed[idx]"
|
||||
:colour="getHSLColourFor(item.text+item.offset)"
|
||||
:readonly="readonly"
|
||||
>
|
||||
<template v-slot:append v-if="editableFieldList && !readonly">
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Remove this property"
|
||||
>
|
||||
<v-icon
|
||||
color="error"
|
||||
@click="removeFixed(idx)"
|
||||
>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dougal-fixed-string-text>
|
||||
|
||||
<v-row dense no-gutters v-if="editableFieldList && !readonly">
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
label="Add fixed text"
|
||||
hint="Enter text"
|
||||
:error-messages="fieldNameErrors"
|
||||
v-model="fixedName"
|
||||
@keydown.enter.prevent="addFixed"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
label="From position"
|
||||
hint="Enter offset"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="fixedOffset"
|
||||
:readonly="readonly"
|
||||
append-outer-icon="mdi-plus-circle"
|
||||
>
|
||||
<template v-slot:append-outer>
|
||||
<v-icon
|
||||
color="primary"
|
||||
:disabled="!fixedName"
|
||||
@click="addFixed"
|
||||
>mdi-plus</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
|
||||
|
||||
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.multiline {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.multiline >>> .line-number {
|
||||
display: inline-block;
|
||||
font-size: 75%;
|
||||
width: 5ex;
|
||||
margin-inline-end: 1ex;
|
||||
text-align: right;
|
||||
border: none;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-field {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-fixed {
|
||||
padding-inline: 1px;
|
||||
border: 1px dashed;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-mismatch {
|
||||
padding-inline: 1px;
|
||||
border: 2px solid red !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { getHSLColourFor } from '@/lib/hsl'
|
||||
import DougalFixedStringDecoderField from './fixed-string-decoder-field'
|
||||
import DougalFixedStringText from './fixed-string-text'
|
||||
|
||||
export default {
|
||||
name: "DougalFixedStringDecoder",
|
||||
|
||||
components: {
|
||||
DougalFixedStringDecoderField,
|
||||
DougalFixedStringText
|
||||
},
|
||||
|
||||
mixins: [
|
||||
{
|
||||
methods: {
|
||||
getHSLColourFor
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
props: {
|
||||
text: { type: String, default: "" },
|
||||
fixed: { type: Array, default: () => [] },
|
||||
fields: { type: Object, default: () => ({}) },
|
||||
multiline: Boolean,
|
||||
numberedLines: [ Boolean, Number ],
|
||||
maxHeight: String,
|
||||
editableFieldList: { type: Boolean, default: true },
|
||||
readonly: Boolean,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
label: String,
|
||||
hint: String,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
//< The reason for not using this.text directly is that at some point
|
||||
//< we might extend this component to allow editing the sample text.
|
||||
text_: "",
|
||||
//< The value of a fixed string that should be always present at a specific position
|
||||
fixedName: "",
|
||||
fixedOffset: 0,
|
||||
//< The name of a new field to add.
|
||||
fieldName: ""
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
/** Whether to treat the sample text as multiline.
|
||||
*/
|
||||
isMultiline () {
|
||||
return this.multiline === true || this.text.includes("\n");
|
||||
},
|
||||
|
||||
/* Return the fields as an array sorted by offset
|
||||
*/
|
||||
parts () {
|
||||
// return Object.entries(this.fields).sort( (a, b) => a[1].offset - b[1].offset );
|
||||
return [
|
||||
...Object.entries(this.fields),
|
||||
...this.fixed.map(i => [ i.text + i.offset, {...i, length: i.text?.length} ])
|
||||
].sort( (a, b) => {
|
||||
const offset_a = a.offset ?? a[1].offset;
|
||||
const offset_b = b.offset ?? b[1].offset;
|
||||
return a - b;
|
||||
})
|
||||
},
|
||||
|
||||
/* Transform this.parts into {start, end} intervals.
|
||||
*/
|
||||
chunks () {
|
||||
const chunks = [];
|
||||
const chunk_num = 0;
|
||||
for (const [name, part] of this.parts) {
|
||||
const chunk = {};
|
||||
chunk.start = part.offset;
|
||||
chunk.end = part.offset + part.length - 1;
|
||||
//chunk.text = this.text_.slice(chunk.start, chunk.end);
|
||||
chunk.colour = this.getHSLColourFor(name)
|
||||
chunk.class = part.text ? "fixed" : "field";
|
||||
chunk.text = part.text;
|
||||
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
},
|
||||
|
||||
multilineElementStyle () {
|
||||
if (this.maxHeight) {
|
||||
return `max-height: ${this.maxHeight};`;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
/** Return a colourised HTML version of this.text.
|
||||
*/
|
||||
html () {
|
||||
if (!this.text_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isMultiline) {
|
||||
if (typeof this.numberedLines == "number" || this.numberedLines) {
|
||||
const offset = typeof this.numberedLines == "number" ? Math.abs(this.numberedLines) : 0;
|
||||
return this.text_.split("\n").map( (line, idx) =>
|
||||
this.numberLine(offset+idx, this.renderTextLine(line))).join("<br/>");
|
||||
} else {
|
||||
return this.text_.split("\n").map(this.renderTextLine).join("<br/>");
|
||||
}
|
||||
} else {
|
||||
return this.renderTextLine(this.text_);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
fieldNameErrors () {
|
||||
return this.parts.find( i => i[0] == this.fieldName )
|
||||
? "A field with this name already exists"
|
||||
: null;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
text () {
|
||||
if (this.text != this.text_) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
addFixed () {
|
||||
if (this.fixedName) {
|
||||
const fixed = [
|
||||
...this.fixed,
|
||||
{ text: this.fixedName, offset: this.fixedOffset }
|
||||
];
|
||||
fixed.sort( (a, b) => a.offset - b.offset );
|
||||
this.fixedName = null;
|
||||
this.fixedOffset = 0;
|
||||
this.$emit("update:fixed", fixed);
|
||||
}
|
||||
},
|
||||
|
||||
addField () {
|
||||
if (!this.fieldNameErrors) {
|
||||
this.$emit("update:fields", {
|
||||
...this.fields,
|
||||
[this.fieldName]: { offset: 0, length: 0 }
|
||||
});
|
||||
this.fieldName = "";
|
||||
}
|
||||
},
|
||||
|
||||
// NOTE Not used
|
||||
updateField (field, key, value) {
|
||||
const fields = {
|
||||
...this.fields,
|
||||
[field]: {
|
||||
...this.fields[field],
|
||||
[key]: value
|
||||
}
|
||||
};
|
||||
this.$emit("update:fields", fields);
|
||||
},
|
||||
|
||||
removeField (key) {
|
||||
const fields = {...this.fields};
|
||||
delete fields[key];
|
||||
this.$emit("update:fields", fields);
|
||||
},
|
||||
|
||||
removeFixed (idx) {
|
||||
const fixed = [...this.fixed];
|
||||
fixed.splice(idx, 1);
|
||||
//fixed.sort( (a, b) => a.offset - b.offset );
|
||||
this.$emit("update:fixed", fixed);
|
||||
},
|
||||
|
||||
/** Return an HSL colour as a function of an input value
|
||||
* `str`.
|
||||
*/
|
||||
xgetHSLColourFor () {
|
||||
console.log("WILL BE DEFINED ON MOUNT");
|
||||
},
|
||||
|
||||
/** Return a `<span>` opening tag.
|
||||
*/
|
||||
style (name, colour) {
|
||||
return colour
|
||||
? `<span class="${name}" style="color:${colour};border-color:${colour}">`
|
||||
: `<span class="${name}">`;
|
||||
},
|
||||
|
||||
/** Return an array of the intervals that intersect `pos`.
|
||||
* May be empty.
|
||||
*/
|
||||
chunksFor (pos) {
|
||||
return this.chunks.filter( chunk =>
|
||||
pos >= chunk.start &&
|
||||
pos <= chunk.end
|
||||
)
|
||||
},
|
||||
|
||||
/*
|
||||
* Algorithm:
|
||||
*
|
||||
* Go through every character of one line of text and determine in which
|
||||
* part(s) it falls in, if any. Collect adjacent same parts into <span/>
|
||||
* elements.
|
||||
*/
|
||||
renderTextLine (text) {
|
||||
const parts = [];
|
||||
|
||||
let prevStyle;
|
||||
|
||||
for (const pos in text) {
|
||||
const chunks = this.chunksFor(pos);
|
||||
const isEmpty = chunks.length == 0;
|
||||
const isOverlap = chunks.length > 1;
|
||||
const isMismatch = chunks[0]?.text &&
|
||||
(text.substring(chunks[0].start, chunks[0].end+1) != chunks[0].text)
|
||||
|
||||
const style = isEmpty
|
||||
? this.style("chunk-empty")
|
||||
: isMismatch
|
||||
? this.style("chunk-mismatch", chunks[0].colour)
|
||||
: isOverlap
|
||||
? this.style("chunk-overlap")
|
||||
: this.style("chunk-"+chunks[0].class, chunks[0].colour);
|
||||
|
||||
if (style != prevStyle) {
|
||||
if (prevStyle) {
|
||||
parts.push("</span>");
|
||||
}
|
||||
parts.push(style);
|
||||
}
|
||||
parts.push(text[pos]);
|
||||
prevStyle = style;
|
||||
}
|
||||
|
||||
if (parts.length) {
|
||||
parts.push("</span>");
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
},
|
||||
|
||||
numberLine (number, line) {
|
||||
return `<span class="line-number">${number}</span>${line}`;
|
||||
},
|
||||
|
||||
setText (v) {
|
||||
//console.log(v);
|
||||
this.text_ = v;
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.text_ = this?.text.replaceAll("\r", "");
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<v-row dense no-gutters>
|
||||
|
||||
<v-col cols="1">
|
||||
<slot name="prepend"></slot>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-chip outlined label small :color="colour" style="border: 1px dashed">{{value.text}}</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="From"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="value.offset"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
</v-col>
|
||||
|
||||
<v-col cols="1">
|
||||
<slot name="append"></slot>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input >>> .chunk {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "DougalFixedStringText",
|
||||
|
||||
components: {
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
colour: String,
|
||||
readonly: Boolean,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
name_: "",
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
name () {
|
||||
if (this.name != this.name_) {
|
||||
this.name_ = this.name;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
addField () {
|
||||
if (!this.fieldNameErrors) {
|
||||
this.$emit("update:fields", {
|
||||
...this.fields,
|
||||
[this.fieldName]: { offset: 0, length: 0 }
|
||||
});
|
||||
this.fieldName = "";
|
||||
}
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.text_ = this.text;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<v-card flat elevation="0">
|
||||
<v-card-title v-if="title">{{ title }}</v-card-title>
|
||||
<v-card-subtitle v-if="subtitle">{{ subtitle }}</v-card-subtitle>
|
||||
<v-card-text>
|
||||
|
||||
<v-tabs v-model="viewTab">
|
||||
<v-tab>Text</v-tab>
|
||||
<v-tab>Parsed</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-items v-model="viewTab">
|
||||
<v-tab-item>
|
||||
<v-simple-table dense class="text">
|
||||
<template v-slot:default>
|
||||
<colgroup v-if="showLineNumbers">
|
||||
<col class="line_no"/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="line_no">
|
||||
<v-simple-checkbox
|
||||
v-ripple
|
||||
off-icon="mdi-format-list-numbered"
|
||||
title="Show line numbers"
|
||||
v-model="showLineNumbers"
|
||||
>
|
||||
</v-simple-checkbox>
|
||||
</th>
|
||||
<th v-for="(header, idx) in headers" :key="idx"
|
||||
:style="`color:${header.colour};`"
|
||||
>
|
||||
{{ header.text }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, ridx) in rows" :key="ridx">
|
||||
<td class="line_no"">
|
||||
<small v-if="showLineNumbers">
|
||||
{{ ridx + (typeof numberedLines == "number" ? numberedLines : 0)+1 }}
|
||||
</small>
|
||||
</td>
|
||||
<td v-for="(cell, cidx) in row" :key="cidx"
|
||||
:style="`background-color:${cell.colour};`"
|
||||
>
|
||||
{{ cell.text }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item>
|
||||
<!-- Parsed view -->
|
||||
<v-simple-table dense class="parsed">
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
title="The line along which the vessel will nominally sail"
|
||||
>Sail line</th>
|
||||
<th
|
||||
title="Whether the line will be acquired in the incrementing or decrementing shot points direction"
|
||||
>Direction</th>
|
||||
<th
|
||||
title="Whether the line is planned to be acquired. Some lines may be in the preplot but not intended to be shot in a particular campaign"
|
||||
>Acquire?</th>
|
||||
<th
|
||||
title="The source lines that will be shot from this vessel line. Typically there is one source line per source array."
|
||||
>Source lines</th>
|
||||
<th
|
||||
title="Any general remarks concerning this sail line (supports Markdown)"
|
||||
>Remarks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(line, line_no) in saillines" :key="line_no">
|
||||
<td>{{ line_no }}</td>
|
||||
<td v-if="line.incr" title="Incrementing">▲</td>
|
||||
<td v-else title="Decrementing">▼</td>
|
||||
<td v-if="line.ntba" title="Not to be acquired" class="ko">✘</td>
|
||||
<td v-else title="Line acquisition planned" class="ok">✔</td>
|
||||
<td v-html="line.source_line.join('<br/>')"></td>
|
||||
<td v-if="line['meta.colour']"
|
||||
:style="`background-color:${line['meta.colour']};`"
|
||||
v-html="$options.filters.markdown(line.remarks)"></td>
|
||||
<td v-else
|
||||
v-html="$options.filters.markdown(line.remarks)"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/*.v-data-table table tbody tr td*/
|
||||
.text th {
|
||||
border: 1px solid hsl(0, 0%, 33.3%);
|
||||
}
|
||||
|
||||
.text td {
|
||||
border-inline: 1px solid hsl(0, 0%, 33.3%);
|
||||
}
|
||||
|
||||
.parsed td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.line_no {
|
||||
text-align: right;
|
||||
width: 4ex;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ok {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.ko {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { parse } from 'csv-parse/sync'
|
||||
import { getHSLColourFor } from '@/lib/hsl'
|
||||
import truncateText from '@/lib/truncate-text'
|
||||
|
||||
export default {
|
||||
name: "DougalSaillinesStringDecoder",
|
||||
|
||||
components: {
|
||||
},
|
||||
|
||||
props: {
|
||||
text: String,
|
||||
//fields: Object,
|
||||
//delimiter: String,
|
||||
headerRow: { type: [ Boolean, Number ], default: false},
|
||||
numberedLines: [ Boolean, Number ],
|
||||
maxHeight: String,
|
||||
editableFieldList: { type: Boolean, default: true },
|
||||
readonly: Boolean,
|
||||
title: String,
|
||||
subtitle: String
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
delimiter: ",",
|
||||
showLineNumbers: null,
|
||||
text_: "",
|
||||
viewTab: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
cells () {
|
||||
return parse(this.text_, {delimiter: this.delimiter, trim: true});
|
||||
},
|
||||
|
||||
headers () {
|
||||
return this.cells[0]?.map(cell => ({
|
||||
text: cell,
|
||||
colour: this.getHSLColourFor(cell),
|
||||
backgroundColour: this.getHSLColourFor(cell, 0.2),
|
||||
})) ?? [];
|
||||
},
|
||||
|
||||
rows () {
|
||||
return [...this.cells].slice(1).map(r =>
|
||||
r.map( (c, ι) => ({
|
||||
text: truncateText(c),
|
||||
colour: this.headers[ι]?.backgroundColour
|
||||
})));
|
||||
},
|
||||
|
||||
/*
|
||||
* A saillines object looks like:
|
||||
*
|
||||
* {
|
||||
* [sail_line]: {
|
||||
* incr: true, // or false
|
||||
* ntba: true, // or false
|
||||
* remarks: "",
|
||||
* source_line: [ 1000, 1001, …],
|
||||
* "meta.colour": ""
|
||||
* },
|
||||
* …
|
||||
* }
|
||||
*/
|
||||
saillines () {
|
||||
// Return an array of the column numbers
|
||||
// corresponding to `key`.
|
||||
// This file accepts duplicate column numbers,
|
||||
// notably for `source_line`.
|
||||
const key_indices = (key) =>
|
||||
this.headers.reduce( (acc, cur, ι) => {
|
||||
if (cur.text == key) {
|
||||
acc.push(ι)
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// 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) => to_bool(v, true),
|
||||
ntba: (v) => to_bool(v, false),
|
||||
remarks: (v) => (v === undefined || v === null) ? "" : String,
|
||||
source_line: Number,
|
||||
};
|
||||
return transformers[key] ?? String;
|
||||
};
|
||||
|
||||
// This is the saillines object
|
||||
const lines = {};
|
||||
|
||||
// The column numbers for each property
|
||||
const columns = keys.map( k => [ k, key_indices(k) ] );
|
||||
|
||||
// The column number for the sail_line property, which
|
||||
// we use as a key.
|
||||
const sail_line_idx = key_indices("sail_line")[0];
|
||||
|
||||
// Transform each line in the input file into a
|
||||
// sailline object (just for display purposes,
|
||||
// this is not exactly how the server will do it).
|
||||
for (const row of this.rows) {
|
||||
const sail_line = row[sail_line_idx]?.text;
|
||||
const values = columns.map(i => [
|
||||
i[0],
|
||||
i[0] == "source_line"
|
||||
? i[1].map(idx => transformer(i[0])(row[idx]?.text))
|
||||
: transformer(i[0])(row[i[1][0]]?.text)
|
||||
]);
|
||||
|
||||
lines[sail_line] = Object.fromEntries(values);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
text () {
|
||||
if (this.text != this.text_) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
|
||||
numberedLines (cur, prev) {
|
||||
if (cur != prev) {
|
||||
this.showLineNumbers = typeof cur == "number" || cur;
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
|
||||
getHSLColourFor: getHSLColourFor.bind(this),
|
||||
|
||||
numberLine (number, line) {
|
||||
return `<span class="line-number">${number}</span>${line}`;
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.text_ = this.text.replaceAll("\r", "");
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<v-row dense no-gutters>
|
||||
|
||||
<v-col>
|
||||
<slot name="prepend"></slot>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-chip v-if="value.item && !readonly"
|
||||
outlined
|
||||
label
|
||||
small
|
||||
:color="colour"
|
||||
:title="description"
|
||||
>{{name}}</v-chip>
|
||||
<v-select v-else-if="items.length && !readonly"
|
||||
label="Item"
|
||||
:items=items
|
||||
v-model="value.item"
|
||||
dense
|
||||
title="Select an item to use as a field"
|
||||
></v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-select v-if="type == 'boolean'"
|
||||
label="Condition"
|
||||
:items="[true, false]"
|
||||
v-model="value.when"
|
||||
dense
|
||||
title="Use this configuration only when the value of this item matches the selected state. This allows the user to configure different values for true and false conditions."
|
||||
></v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-text-field v-if="type == 'boolean' || type == 'text'"
|
||||
class="ml-3"
|
||||
dense
|
||||
label="Value"
|
||||
v-model="value.value"
|
||||
title="This literal text will be inserted at the designated position"
|
||||
></v-text-field>
|
||||
<v-menu v-else-if="type == 'number'"
|
||||
max-width="600"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-chip
|
||||
class="ml-3"
|
||||
small
|
||||
:light="$vuetify.theme.isDark"
|
||||
:dark="!$vuetify.theme.isDark"
|
||||
:color="value.scale_offset != null || value.scale_multiplier != null ? 'primary' : ''"
|
||||
:title="`Number scaling${ value.scale_offset != null ? ('\nOffset: ' + value.scale_offset) : '' }${ value.scale_multiplier != null ? ('\nMultiplier: ' + value.scale_multiplier) : ''}`"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon small>mdi-ruler</v-icon>
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<v-card rounded outlined>
|
||||
<v-card-text>
|
||||
<v-row dense no-gutters>
|
||||
<v-text-field
|
||||
type="number"
|
||||
dense
|
||||
clearable
|
||||
label="Offset"
|
||||
title="Offset the value by this amount (after scaling)"
|
||||
v-model.number="value.scale_offset"
|
||||
></v-text-field>
|
||||
</v-row>
|
||||
<v-row dense no-gutters>
|
||||
<v-text-field
|
||||
type="number"
|
||||
dense
|
||||
clearable
|
||||
label="Scale"
|
||||
title="Mutiply the value by this amount (before scaling)"
|
||||
v-model.number="value.scale_multiplier"
|
||||
></v-text-field>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="From"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="value.offset"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="Length"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="value.length"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-menu v-if="value.length > 1"
|
||||
max-width="600"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
:disabled="!(value.length>1)"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-chip
|
||||
class="ml-3"
|
||||
small
|
||||
:light="$vuetify.theme.isDark"
|
||||
:dark="!$vuetify.theme.isDark"
|
||||
title="Text alignment"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
:disabled="!(value.length>1)"
|
||||
>
|
||||
<v-icon small v-if="value.pad_side=='right'">mdi-format-align-left</v-icon>
|
||||
<v-icon small v-else-if="value.pad_side=='left'">mdi-format-align-right</v-icon>
|
||||
<v-icon small v-else>mdi-format-align-justify</v-icon>
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<v-card rounded outlined>
|
||||
<v-card-text>
|
||||
<v-row dense no-gutters>
|
||||
<v-select
|
||||
label="Alignment"
|
||||
clearable
|
||||
:items='[{text:"Left", value:"right"}, {text:"Right", value:"left"}]'
|
||||
v-model="value.pad_side"
|
||||
></v-select>
|
||||
</v-row>
|
||||
<v-row dense no-gutters v-if="value.pad_side">
|
||||
<v-text-field
|
||||
dense
|
||||
label="Pad character"
|
||||
title="Fill the width of the field on the opposite side by padding with this character"
|
||||
v-model="value.pad_string"
|
||||
></v-text-field>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<slot name="append"></slot>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input >>> .chunk {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DougalFixedStringEncoderField",
|
||||
|
||||
components: {
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
properties: Object,
|
||||
colour: String,
|
||||
readonly: Boolean,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
"value.value": function (value, old) {
|
||||
if (value != null && String(value).length > this.value.length) {
|
||||
this.value.length = String(value).length;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
field: {
|
||||
get () {
|
||||
return this.value;
|
||||
},
|
||||
set (v) {
|
||||
console.log("input", v);
|
||||
this.$emit("input", v);
|
||||
}
|
||||
},
|
||||
|
||||
item () {
|
||||
return this.properties?.[this.value?.item] ?? {};
|
||||
},
|
||||
|
||||
items () {
|
||||
return Object.entries(this.properties).map(i => ({text: i[1].summary ?? i[0], value: i[0]}))
|
||||
},
|
||||
|
||||
name () {
|
||||
// TODO Use properties[item].summary or similar
|
||||
return this.item?.summary ?? this.value.item ?? "???";
|
||||
},
|
||||
|
||||
type () {
|
||||
return this.item?.type ?? typeof this.value?.item ?? "undefined";
|
||||
},
|
||||
|
||||
description () {
|
||||
return this.item?.description;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
reset () {
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<v-input
|
||||
class="v-text-field"
|
||||
:hint="hint"
|
||||
persistent-hint
|
||||
:value="text"
|
||||
>
|
||||
<label
|
||||
class="v-label"
|
||||
:class="[ $vuetify.theme.isDark && 'theme--dark', text && text.length && 'v-label--active' ]"
|
||||
style="left: 0px; right: auto; position: absolute;"
|
||||
>{{ label }}</label>
|
||||
<div class="input" slot="default"
|
||||
v-html="html"
|
||||
>
|
||||
</div>
|
||||
<template slot="append">
|
||||
<v-menu
|
||||
scrollable
|
||||
offset-y
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
|
||||
<template v-slot:activator="{on, attrs}">
|
||||
<v-btn
|
||||
icon
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon title="Configure sample values">mdi-list-box-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>Sample values</v-card-title>
|
||||
<v-card-subtitle>Enter sample values to test your configuration</v-card-subtitle>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
|
||||
<v-container>
|
||||
<v-row v-for="(prop, key) in properties" :key="key">
|
||||
<template v-if="prop.type == 'boolean'">
|
||||
<v-col cols="6" align-self="center">
|
||||
<v-chip
|
||||
outlined
|
||||
label
|
||||
small
|
||||
:color="getHSLColourFor(key)"
|
||||
:title="prop.description"
|
||||
>{{prop.summary || key}}</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="6" align-self="center">
|
||||
<v-simple-checkbox v-model="values[key]"></v-simple-checkbox>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-else-if="key != 'text'">
|
||||
<v-col cols="6" align-self="center">
|
||||
<v-chip
|
||||
outlined
|
||||
label
|
||||
small
|
||||
:color="getHSLColourFor(key)"
|
||||
:title="prop.description"
|
||||
>{{prop.summary || key}}</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="6" align-self="center">
|
||||
<v-text-field v-if="prop.type == 'number'"
|
||||
:type="prop.type"
|
||||
:label="prop.summary || key"
|
||||
:hint="prop.description"
|
||||
v-model.number="values[key]"
|
||||
></v-text-field>
|
||||
<v-text-field v-else
|
||||
:type="prop.type"
|
||||
:label="prop.summary || key"
|
||||
:hint="prop.description"
|
||||
v-model="values[key]"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
</v-menu>
|
||||
</template>
|
||||
<v-icon slot="prepend">mdi-list</v-icon>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
white-space-collapse: preserve;
|
||||
}
|
||||
|
||||
.multiline {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.multiline >>> .line-number {
|
||||
display: inline-block;
|
||||
font-size: 75%;
|
||||
width: 5ex;
|
||||
margin-inline-end: 1ex;
|
||||
text-align: right;
|
||||
border: none;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-field {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-fixed {
|
||||
padding-inline: 1px;
|
||||
border: 1px dashed;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.input >>> .chunk-mismatch {
|
||||
padding-inline: 1px;
|
||||
border: 2px solid red !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { getHSLColourFor } from '@/lib/hsl'
|
||||
|
||||
export default {
|
||||
name: "DougalFixedStringEncoderSample",
|
||||
|
||||
components: {
|
||||
},
|
||||
|
||||
mixins: [
|
||||
{
|
||||
methods: {
|
||||
getHSLColourFor
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
props: {
|
||||
properties: { type: Object, default: () => ({}) },
|
||||
fields: { type: Array, default: () => [] },
|
||||
values: { type: Object, default: () => ({}) },
|
||||
readonly: Boolean,
|
||||
label: String,
|
||||
hint: String,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
chunks () {
|
||||
const properties = this.properties;
|
||||
const fields = this.fields;
|
||||
const values = this.values;
|
||||
const str = "";
|
||||
const chunks = [];
|
||||
|
||||
for (const field of fields) {
|
||||
const value = this.fieldValue(properties, field, values);
|
||||
|
||||
if (value != null) {
|
||||
const chunk = {
|
||||
start: field.offset,
|
||||
end: field.offset + field.length - 1,
|
||||
colour: this.getHSLColourFor(field.item),
|
||||
class: field.item == "text" ? "fixed" : "field",
|
||||
text: value
|
||||
}
|
||||
chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
},
|
||||
|
||||
text () {
|
||||
return this.sample(this.properties, this.fields, this.values);
|
||||
},
|
||||
|
||||
html () {
|
||||
return this.renderTextLine(this.text);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
fieldValue (properties, field, values) {
|
||||
let value;
|
||||
|
||||
if (field.item == "text") {
|
||||
value = field.value;
|
||||
} else if (properties[field.item]?.type == "boolean") {
|
||||
if (values[field.item] === field.when) {
|
||||
value = field.value;
|
||||
}
|
||||
} else {
|
||||
value = values[field.item];
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
|
||||
if (properties[field.item]?.type == "number") {
|
||||
if (field.scale_multiplier != null) {
|
||||
value *= field.scale_multiplier;
|
||||
}
|
||||
if (field.scale_offset != null) {
|
||||
value += field.scale_offset;
|
||||
}
|
||||
|
||||
if (field.format == "integer") {
|
||||
value = Math.round(value);
|
||||
}
|
||||
}
|
||||
|
||||
value = String(value);
|
||||
if (field.pad_side == "left") {
|
||||
value = value.padStart(field.length, field.pad_string ?? " ");
|
||||
} else if (field.pad_side == "right") {
|
||||
value = value.padEnd(field.length, field.pad_string ?? " ");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
},
|
||||
|
||||
sample (properties, fields, values, str = "") {
|
||||
|
||||
const length = fields.reduce( (acc, cur) => (cur.offset + cur.length) > acc ? (cur.offset + cur.length) : acc, str.length )
|
||||
|
||||
str = str.padEnd(length);
|
||||
|
||||
for (const field of fields) {
|
||||
//console.log("FIELD", field);
|
||||
const value = this.fieldValue(properties, field, values);
|
||||
if (value != null) {
|
||||
str = str.slice(0, field.offset) + value + str.slice(field.offset + field.length);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
},
|
||||
|
||||
/** Return a `<span>` opening tag.
|
||||
*/
|
||||
style (name, colour) {
|
||||
return colour
|
||||
? `<span class="${name}" style="color:${colour};border-color:${colour}">`
|
||||
: `<span class="${name}">`;
|
||||
},
|
||||
|
||||
/** Return an array of the intervals that intersect `pos`.
|
||||
* May be empty.
|
||||
*/
|
||||
chunksFor (pos) {
|
||||
return this.chunks.filter( chunk =>
|
||||
pos >= chunk.start &&
|
||||
pos <= chunk.end
|
||||
)
|
||||
},
|
||||
|
||||
/*
|
||||
* Algorithm:
|
||||
*
|
||||
* Go through every character of one line of text and determine in which
|
||||
* part(s) it falls in, if any. Collect adjacent same parts into <span/>
|
||||
* elements.
|
||||
*/
|
||||
renderTextLine (text) {
|
||||
const parts = [];
|
||||
|
||||
let prevStyle;
|
||||
|
||||
for (const pos in text) {
|
||||
const chunks = this.chunksFor(pos);
|
||||
const isEmpty = chunks.length == 0;
|
||||
const isOverlap = chunks.length > 1;
|
||||
const isMismatch = chunks[0]?.text &&
|
||||
(text.substring(chunks[0].start, chunks[0].end+1) != chunks[0].text);
|
||||
|
||||
const style = isEmpty
|
||||
? this.style("chunk-empty")
|
||||
: isMismatch
|
||||
? this.style("chunk-mismatch", chunks[0].colour)
|
||||
: isOverlap
|
||||
? this.style("chunk-overlap")
|
||||
: this.style("chunk-"+chunks[0].class, chunks[0].colour);
|
||||
|
||||
if (style != prevStyle) {
|
||||
if (prevStyle) {
|
||||
parts.push("</span>");
|
||||
}
|
||||
parts.push(style);
|
||||
}
|
||||
parts.push(text[pos]);
|
||||
prevStyle = style;
|
||||
}
|
||||
|
||||
if (parts.length) {
|
||||
parts.push("</span>");
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<v-card flat elevation="0">
|
||||
<v-card-title v-if="title">{{ title }}</v-card-title>
|
||||
<v-card-subtitle v-if="subtitle">{{ subtitle }}</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
|
||||
<!-- Sample text -->
|
||||
|
||||
<dougal-fixed-string-encoder-sample
|
||||
:label="label"
|
||||
:hint="hint"
|
||||
:properties="properties"
|
||||
:fields="fields"
|
||||
:values.sync="values"
|
||||
></dougal-fixed-string-encoder-sample>
|
||||
|
||||
<!-- Fields -->
|
||||
|
||||
<v-container>
|
||||
|
||||
<v-row no-gutters class="mb-2">
|
||||
<h4>Fields</h4>
|
||||
</v-row>
|
||||
|
||||
<dougal-fixed-string-encoder-field v-for="(field, key) in fields" :key="key"
|
||||
v-model="fields[key]"
|
||||
:properties="properties"
|
||||
:colour="getHSLColourFor(field.item)"
|
||||
:readonly="readonly"
|
||||
>
|
||||
<template v-slot:append v-if="editableFieldList && !readonly">
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Remove this field"
|
||||
>
|
||||
<v-icon
|
||||
color="error"
|
||||
@click="removeField(key)"
|
||||
>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dougal-fixed-string-encoder-field>
|
||||
|
||||
<v-row no-gutters class="mb-2" v-if="editableFieldList && !readonly">
|
||||
<h4>Add new field</h4>
|
||||
</v-row>
|
||||
|
||||
<dougal-fixed-string-encoder-field v-if="editableFieldList && !readonly"
|
||||
v-model="newField"
|
||||
:properties="properties"
|
||||
:colour="getHSLColourFor(newField.item)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-btn v-if="isFieldDirty(newField)"
|
||||
top
|
||||
text
|
||||
small
|
||||
title="Reset"
|
||||
>
|
||||
<v-icon
|
||||
color="warning"
|
||||
@click="resetField(newField)"
|
||||
>mdi-backspace-reverse-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Add field"
|
||||
:disabled="isFieldValid(newField) !== true"
|
||||
>
|
||||
<v-icon
|
||||
color="primary"
|
||||
@click="addField(newField)"
|
||||
>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dougal-fixed-string-encoder-field>
|
||||
|
||||
</v-container>
|
||||
|
||||
|
||||
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-field {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-fixed {
|
||||
padding-inline: 1px;
|
||||
border: 1px dashed;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.input >>> .chunk-mismatch {
|
||||
padding-inline: 1px;
|
||||
border: 2px solid red !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { getHSLColourFor } from '@/lib/hsl'
|
||||
import DougalFixedStringEncoderField from './fixed-string-encoder-field'
|
||||
import DougalFixedStringEncoderSample from './fixed-string-encoder-sample'
|
||||
|
||||
export default {
|
||||
name: "DougalFixedStringEncoder",
|
||||
|
||||
components: {
|
||||
DougalFixedStringEncoderField,
|
||||
DougalFixedStringEncoderSample
|
||||
},
|
||||
|
||||
mixins: [
|
||||
{
|
||||
methods: {
|
||||
getHSLColourFor
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
props: {
|
||||
properties: { type: Object },
|
||||
fields: { type: Array },
|
||||
values: { type: Object },
|
||||
editableFieldList: { type: Boolean, default: true },
|
||||
readonly: Boolean,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
label: String,
|
||||
hint: String,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
//< The reason for not using this.text directly is that at some point
|
||||
//< we might extend this component to allow editing the sample text.
|
||||
text_: "",
|
||||
//< The value of a fixed string that should be always present at a specific position
|
||||
fixedName: "",
|
||||
fixedOffset: 0,
|
||||
//< The name of a new field to add.
|
||||
fieldName: "",
|
||||
newField: {
|
||||
item: null,
|
||||
when: null,
|
||||
offset: null,
|
||||
length: null,
|
||||
value: null,
|
||||
pad_side: null,
|
||||
pad_string: null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
chunks () {
|
||||
const properties = this.properties;
|
||||
const fields = this.fields;
|
||||
const values = this.values;
|
||||
const str = "";
|
||||
const chunks = [];
|
||||
|
||||
for (const field of fields) {
|
||||
|
||||
//console.log("FIELD", structuredClone(field));
|
||||
//console.log("VALUES DATA", values[field.item]);
|
||||
let value;
|
||||
|
||||
if (field.item == "text") {
|
||||
value = field.value;
|
||||
} else if (properties[field.item]?.type == "boolean") {
|
||||
if (values[field.item] === field.when) {
|
||||
value = field.value;
|
||||
}
|
||||
} else {
|
||||
value = values[field.item];
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
|
||||
value = String(value);
|
||||
if (field.pad_side == "left") {
|
||||
value = value.padStart(field.length, field.pad_string);
|
||||
} else {
|
||||
value = value.padEnd(field.length, field.pad_string);
|
||||
}
|
||||
|
||||
const chunk = {
|
||||
start: field.offset,
|
||||
end: field.offset + field.length - 1,
|
||||
colour: this.getHSLColourFor(field.item),
|
||||
class: field.item == "text" ? "fixed" : "field",
|
||||
text: value
|
||||
}
|
||||
|
||||
//console.log("CHUNK", chunk);
|
||||
chunks.push(chunk);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return chunks;
|
||||
},
|
||||
|
||||
html () {
|
||||
return this.renderTextLine(this.sample(this.properties, this.fields, this.values));
|
||||
//return this.sample(this.properties, this.fields, this.values);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
isFieldDirty (field) {
|
||||
return Object.entries(field).reduce( (acc, cur) => cur[1] === null ? acc : true, false );
|
||||
},
|
||||
|
||||
isFieldValid (field) {
|
||||
if (!field.item) return "Missing item";
|
||||
if (typeof field.offset !== "number" || field.offset < 0) return "Missing offset";
|
||||
if (typeof field.length !== "number" || field.length < 1) return "Missing length";
|
||||
if (!this.properties[field.item]) return "Unrecognised property";
|
||||
if (this.properties[field.item].type == "text" && !field.value?.length) return "Missing value";
|
||||
if (this.properties[field.item].type == "boolean" && !field.value?.length) return "Missing value (boolean)";
|
||||
if(!!field.pad_side && !field.pad_string) return "Missing pad string";
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
resetField (field) {
|
||||
field.item = null;
|
||||
field.when = null;
|
||||
field.offset = null;
|
||||
field.length = null;
|
||||
field.value = null;
|
||||
field.pad_side = null;
|
||||
field.pad_string = null;
|
||||
|
||||
return field;
|
||||
},
|
||||
|
||||
addField (field) {
|
||||
if (this.isFieldValid(field)) {
|
||||
const fields = structuredClone(this.fields);
|
||||
fields.push({...field});
|
||||
this.resetField(field);
|
||||
console.log("update:fields", fields);
|
||||
this.$emit("update:fields", fields);
|
||||
}
|
||||
},
|
||||
|
||||
removeField (key) {
|
||||
console.log("REMOVE", "update:fields", key, this.fields);
|
||||
const fields = structuredClone(this.fields);
|
||||
fields.splice(key, 1);
|
||||
this.$emit("update:fields", fields);
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
max-width="600"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-chip
|
||||
class="ml-3"
|
||||
small
|
||||
:light="$vuetify.theme.isDark"
|
||||
:dark="!$vuetify.theme.isDark"
|
||||
:title="getFriendlyTypeName(value.type)"
|
||||
:color="getHSLColourFor(value.type||'str', .4, .5)"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon small>{{ getTypeIcon(value.type||'str') }}</v-icon>
|
||||
<v-icon small v-if="value.enum"
|
||||
:title="'Values: '+Object.entries(value.enum).map(i => `${i[0]}=${i[1]}`).join('; ')+'\nDefault: '+value.default"
|
||||
>mdi-format-list-group</v-icon>
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<dougal-field-content
|
||||
:readonly="readonly"
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
></dougal-field-content>
|
||||
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DougalFieldContent from './field-content'
|
||||
|
||||
export default {
|
||||
|
||||
name: "DougalFieldContentDialog",
|
||||
|
||||
components: {
|
||||
DougalFieldContent
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
readonly: Boolean
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
getFriendlyTypeName (type) {
|
||||
switch (type) {
|
||||
case "str":
|
||||
return "Text";
|
||||
case "int":
|
||||
return "Integer";
|
||||
case "float":
|
||||
return "Float";
|
||||
case "bool":
|
||||
return "Boolean";
|
||||
default:
|
||||
return type ?? "Text (default)";
|
||||
}
|
||||
},
|
||||
|
||||
getTypeIcon (type) {
|
||||
switch (type) {
|
||||
case "str":
|
||||
return "mdi-format-text-variant";
|
||||
case "int":
|
||||
return "mdi-numeric";
|
||||
case "float":
|
||||
return "mdi-decimal";
|
||||
case "bool":
|
||||
return "mdi-format-list-checks";
|
||||
default:
|
||||
return "mdi-format-text";
|
||||
}
|
||||
},
|
||||
|
||||
getHSLColourFor (str, saturation = 1, lightness = 0.25, offset = 0) {
|
||||
|
||||
function getHash (v) {
|
||||
return [...v].reduce( (acc, cur) => String(cur).charCodeAt(0) + ((acc << 5) - acc), 0 );
|
||||
}
|
||||
|
||||
const h = (getHash(str) + offset) % 360;
|
||||
const s = saturation * 100;
|
||||
const l = this.$vuetify.theme.isDark
|
||||
? (1-lightness) * 100
|
||||
: lightness * 100;
|
||||
|
||||
return `hsl(${h},${s}%,${l}%)`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
242
lib/www/client/source/src/components/fields/field-content.vue
Normal file
242
lib/www/client/source/src/components/fields/field-content.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<v-card flat elevation="0">
|
||||
<v-card-subtitle>Item options</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-select
|
||||
label="Value type"
|
||||
v-model="type"
|
||||
:items="types"
|
||||
value="int"
|
||||
:readonly="readonly"
|
||||
></v-select>
|
||||
|
||||
<v-checkbox
|
||||
label="Enumerated values"
|
||||
v-model="enumerated"
|
||||
:readonly="readonly"
|
||||
></v-checkbox>
|
||||
</v-card-text>
|
||||
|
||||
<template v-if="enumerated">
|
||||
<v-card-subtitle>Valid options</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="(out, key) in value.enum" :key=key
|
||||
>
|
||||
<v-list-item-content class="mr-1">
|
||||
<v-text-field
|
||||
dense
|
||||
hide-details="auto"
|
||||
v-model="key"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-list-item-content>
|
||||
<v-list-item-content class="ml-1">
|
||||
<v-select v-if="type == 'bool'"
|
||||
dense
|
||||
hide-details="auto"
|
||||
:items="[ true, false ]"
|
||||
v-model="value.enum[key]"
|
||||
:readonly="readonly"
|
||||
></v-select>
|
||||
<v-text-field v-else
|
||||
dense
|
||||
hide-details="auto"
|
||||
v-model="value.enum[key]"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-icon
|
||||
small
|
||||
color="error"
|
||||
:disabled="readonly"
|
||||
@click="removeEnum(key)"
|
||||
>mdi-minus-circle</v-icon>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="!readonly"
|
||||
>
|
||||
<v-list-item-content class="mr-1">
|
||||
<v-text-field
|
||||
dense
|
||||
hide-details="auto"
|
||||
label="New input value"
|
||||
v-model="newEnumKey"
|
||||
></v-text-field>
|
||||
</v-list-item-content>
|
||||
<v-list-item-content class="ml-1">
|
||||
<v-select v-if="type == 'bool'"
|
||||
dense
|
||||
hide-details="auto"
|
||||
label="New output value"
|
||||
:items="[ true, false ]"
|
||||
v-model="newEnumValue"
|
||||
></v-select>
|
||||
<v-text-field v-else
|
||||
dense
|
||||
hide-details="auto"
|
||||
label="New output value"
|
||||
v-model="newEnumValue"
|
||||
></v-text-field>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-icon
|
||||
small
|
||||
color="primary"
|
||||
:disabled="!isNewEnumValid"
|
||||
@click="addEnum"
|
||||
>mdi-plus-circle</v-icon>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-select v-if="type == 'bool'"
|
||||
dense
|
||||
hide-details="auto"
|
||||
label="Default value"
|
||||
hint="Value to use if none matches"
|
||||
:items="[ true, false ]"
|
||||
v-model="defaultValue"
|
||||
:readonly="readonly"
|
||||
></v-select>
|
||||
<v-text-field v-else
|
||||
label="Default value"
|
||||
hint="Value to use if none matches"
|
||||
persistent-hint
|
||||
v-model="defaultValue"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-icon
|
||||
small
|
||||
color="secondary"
|
||||
:disabled="readonly"
|
||||
@click="defaultValue = null"
|
||||
>mdi-backspace</v-icon>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
</v-list>
|
||||
|
||||
</v-card-text>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "DougalFieldContent",
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
readonly: Boolean
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
newEnumKey: null,
|
||||
newEnumValue: null,
|
||||
types: [
|
||||
{ text: "Text", value: "str" },
|
||||
{ text: "Integer", value: "int" },
|
||||
{ text: "Float", value: "float" },
|
||||
{ text: "Boolean", value: "bool" },
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
type: {
|
||||
get () {
|
||||
return this.value?.type ?? "str";
|
||||
},
|
||||
|
||||
set (v) {
|
||||
this.$emit("input", {
|
||||
...this.value,
|
||||
type: v
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
enumerated: {
|
||||
get () {
|
||||
return typeof this.value?.enum === "object";
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (v) {
|
||||
this.$emit("input", {
|
||||
enum: {},
|
||||
...this.value
|
||||
})
|
||||
} else {
|
||||
const obj = {...this.value};
|
||||
delete obj.enum;
|
||||
this.$emit("input", obj)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
defaultValue: {
|
||||
|
||||
get () {
|
||||
return this.value?.default;
|
||||
},
|
||||
|
||||
set (v) {
|
||||
this.$emit("input", {
|
||||
...this.value,
|
||||
"default": v
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
isNewEnumValid () {
|
||||
return !!(this.newEnumKey &&
|
||||
!Object.keys(this.value.enum).includes(this.newEnumKey) &&
|
||||
(typeof this.newEnumValue == "boolean" || this.newEnumValue));
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
addEnum () {
|
||||
this.$emit("input", {
|
||||
...this.value,
|
||||
enum: {
|
||||
...this.value.enum,
|
||||
[this.newEnumKey]: this.newEnumValue
|
||||
}
|
||||
});
|
||||
this.newEnumKey = null;
|
||||
this.newEnumValue = null;
|
||||
},
|
||||
|
||||
removeEnum (key) {
|
||||
const obj = {...this.value.enum};
|
||||
delete obj[key];
|
||||
this.$emit("input", {
|
||||
...this.value,
|
||||
enum: obj
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
max-width="600"
|
||||
v-model="open"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-icon
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
:title="title"
|
||||
>mdi-folder-network-outline</v-icon>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title>File picker</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<dougal-file-browser
|
||||
v-model="selected"
|
||||
:mimetypes="mimetypes"
|
||||
:root="root"
|
||||
ref="browser"
|
||||
>
|
||||
</dougal-file-browser>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text @click="save" :disabled="!selected">
|
||||
<v-icon small flat color="primary" class="mr-2">mdi-content-save</v-icon>
|
||||
Ok
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="refresh">
|
||||
<v-icon small flat class="mr-2">mdi-reload</v-icon>
|
||||
Refresh
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="close">
|
||||
<v-icon small flat color="red" class="mr-2">mdi-close</v-icon>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DougalFileBrowser from './file-browser';
|
||||
|
||||
export default {
|
||||
name: "DougalFileBrowserDialog",
|
||||
|
||||
components: { DougalFileBrowser },
|
||||
|
||||
props: [ "path", "mimetypes", "root", "title" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
open: false,
|
||||
selected: ""
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
refresh () {
|
||||
this.$refs.browser.refresh();
|
||||
},
|
||||
|
||||
close () {
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
save () {
|
||||
this.$emit('input', this.selected);
|
||||
this.close();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.selected = this.path;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<v-layout fill-height style="overflow-y:auto;max-height:400px;">
|
||||
<v-progress-circular v-if="loading && !items.length"></v-progress-circular>
|
||||
<v-treeview v-else
|
||||
activatable
|
||||
:active.sync="active"
|
||||
:items="items"
|
||||
item-key="path"
|
||||
item-name="basename"
|
||||
:load-children="readdir"
|
||||
@update:active="activeChanged"
|
||||
style="min-width:100%"
|
||||
>
|
||||
<template v-slot:label="{item}">
|
||||
<div style="cursor:pointer;">
|
||||
{{ item.basename }}
|
||||
</div>
|
||||
</template>
|
||||
</v-treeview>
|
||||
</v-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
function find(haystack, needle) {
|
||||
for (const item of haystack) {
|
||||
if (item.path == needle) {
|
||||
return item;
|
||||
} else if (item.children) {
|
||||
const found = find(item.children, needle);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "DougalFileBrowser",
|
||||
|
||||
props: [ "value", "mimetypes", "root" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
items: [],
|
||||
active: [],
|
||||
selected: null,
|
||||
path: "",
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
dirsAreSelectable () {
|
||||
return !this.mimetypes ||
|
||||
this.mimetypes == "inode/directory" ||
|
||||
(Array.isArray(this.mimetypes) && this.mimetypes.includes("inode/directory"));
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
activeChanged (active) {
|
||||
const candidate = find(this.items, active[0]);
|
||||
if (!this.dirsAreSelectable && this.isDirectory(candidate)) {
|
||||
this.selected = null;
|
||||
} else {
|
||||
this.selected = candidate;
|
||||
}
|
||||
this.$emit("input", this.selected?.path);
|
||||
},
|
||||
|
||||
isDirectory (item) {
|
||||
return item && item["Content-Type"] == "inode/directory";
|
||||
},
|
||||
|
||||
filterMimetypes (item) {
|
||||
if (!this.mimetypes) {
|
||||
return true;
|
||||
} else if (Array.isArray(this.mimetypes)) {
|
||||
return item["Content-Type"] == "inode/directory" ||
|
||||
this.mimetypes.includes(item["Content-Type"].split(";")[0]) ||
|
||||
this.filterGlob(item);
|
||||
} else {
|
||||
return item["Content-Type"] == "inode/directory" ||
|
||||
this.mimetypes == item["Content-Type"].split(";")[0];
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
filterGlob (item) {
|
||||
const globs = (Array.isArray(this.mimetypes)
|
||||
? this.mimetypes
|
||||
: [ this.mimetypes ])
|
||||
.filter(i => /^\*\..+$/.test(i));
|
||||
|
||||
for (const glob of globs) {
|
||||
const ext = (glob.match(/^\*\.(.+)$/)||[])[1];
|
||||
if (item.path.toLowerCase().endsWith(ext.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
async readdir (item) {
|
||||
this.loading = true;
|
||||
const url = `/files/${item? item.path : (this.root || this.path || "")}`;
|
||||
const list = await this.api([url]);
|
||||
this.loading = false;
|
||||
const items = list?.map(item => {
|
||||
if (item["Content-Type"] == "inode/directory") {
|
||||
item.children = [];
|
||||
}
|
||||
item.id = item.path;
|
||||
item.name = item.basename;
|
||||
return item;
|
||||
}).filter(this.filterMimetypes);
|
||||
if (item) {
|
||||
item.children = items;
|
||||
} else {
|
||||
this.items = items;
|
||||
}
|
||||
},
|
||||
|
||||
async refresh () {
|
||||
this.items = []
|
||||
this.$nextTick(this.readdir);
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.value) {
|
||||
this.path = this.value;
|
||||
}
|
||||
this.readdir();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -127,7 +127,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
default:
|
||||
return {
|
||||
editable: false,
|
||||
displayLogo: false
|
||||
displaylogo: false
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -105,7 +105,11 @@ export default {
|
||||
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>`},
|
||||
title: {
|
||||
text: this.data[0]?.x?.length
|
||||
? `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>`
|
||||
: `Final vs raw: no data`
|
||||
},
|
||||
xaxis: {
|
||||
title: "Crossline (m)",
|
||||
showgrid: true,
|
||||
@@ -258,8 +262,13 @@ export default {
|
||||
methods: {
|
||||
|
||||
plot () {
|
||||
Plotly.newPlot(this.$refs.graph, this.data, this.layout, this.config);
|
||||
this.plotted = true;
|
||||
if (this.items?.length) {
|
||||
Plotly.newPlot(this.$refs.graph, this.data, this.layout, this.config);
|
||||
this.plotted = true;
|
||||
} else {
|
||||
Plotly.purge(this.$refs.graph);
|
||||
this.plotted = false;
|
||||
}
|
||||
},
|
||||
|
||||
replot () {
|
||||
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
config () {
|
||||
return {
|
||||
editable: false,
|
||||
displayLogo: false
|
||||
displaylogo: false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -101,6 +101,7 @@ export default {
|
||||
type: "groupby",
|
||||
groups: src_number,
|
||||
styles: [
|
||||
{value: {showlegend: false}},
|
||||
{target: 1, value: {line: {color: "green"}}},
|
||||
{target: 2, value: {line: {color: "red"}}},
|
||||
{target: 3, value: {line: {color: "blue"}}}
|
||||
@@ -141,8 +142,13 @@ export default {
|
||||
methods: {
|
||||
|
||||
plot () {
|
||||
Plotly.newPlot(this.$refs.graph, this.data, this.layout, this.config);
|
||||
this.plotted = true;
|
||||
if (this.items?.length) {
|
||||
Plotly.newPlot(this.$refs.graph, this.data, this.layout, this.config);
|
||||
this.plotted = true;
|
||||
} else {
|
||||
Plotly.purge(this.$refs.graph);
|
||||
this.plotted = false;
|
||||
}
|
||||
},
|
||||
|
||||
replot () {
|
||||
@@ -168,6 +174,9 @@ export default {
|
||||
const src_ids = new Set(src_number);
|
||||
|
||||
for (const src of src_ids) {
|
||||
// Ignore shots without source data
|
||||
if (!src) continue;
|
||||
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-treeview
|
||||
dense
|
||||
activatable
|
||||
hoverable
|
||||
:multiple-active="false"
|
||||
:active.sync="active"
|
||||
:open.sync="open"
|
||||
:items="treeview"
|
||||
style="cursor:pointer;width:100%;"
|
||||
>
|
||||
<template v-slot:prepend="{item}">
|
||||
<template v-if="item.icon">
|
||||
<v-icon
|
||||
small
|
||||
left
|
||||
:title="item.leaf ? item.type : `${item.type} (${item.children.length} children)`"
|
||||
>{{item.icon}}</v-icon>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-slot:label="{item}">
|
||||
<template v-if="!('path' in item)">
|
||||
{{item.name}}
|
||||
</template>
|
||||
<template v-else-if="item.leaf">
|
||||
<v-chip
|
||||
small
|
||||
label
|
||||
outlined
|
||||
:color="item.isArrayItem ? 'secondary' : 'primary'"
|
||||
>
|
||||
{{item.name}}
|
||||
</v-chip>
|
||||
<code class="ml-4" v-if="item.type == 'bigint'">{{item.value+"n"}}</code>
|
||||
<code class="ml-4" v-else-if="item.type == 'boolean'"><b>{{item.value}}</b></code>
|
||||
<code class="ml-4" v-else>{{item.value}}</code>
|
||||
<v-icon v-if="item.type == 'string' && (/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3}([0-9a-fA-F]{2})?)?$/.test(item.value) || item.name == 'colour' || item.name == 'color')"
|
||||
right
|
||||
:color="item.value"
|
||||
>mdi-square</v-icon>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-chip
|
||||
small
|
||||
label
|
||||
outlined
|
||||
:color="item.isArrayItem ? 'secondary' : 'primary'"
|
||||
>
|
||||
{{item.name}}
|
||||
</v-chip>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-slot:append="{item}">
|
||||
<template>
|
||||
<v-icon v-if="item.type == 'array'"
|
||||
small
|
||||
right
|
||||
outline
|
||||
color="primary"
|
||||
title="Add item"
|
||||
@click="itemAddDialog(item)"
|
||||
>mdi-plus</v-icon>
|
||||
<v-icon v-if="item.type == 'object'"
|
||||
small
|
||||
right
|
||||
outline
|
||||
color="primary"
|
||||
title="Add property"
|
||||
@click="itemAddDialog(item)"
|
||||
>mdi-plus</v-icon>
|
||||
<v-icon v-if="item.type == 'boolean'"
|
||||
small
|
||||
right
|
||||
outline
|
||||
color="primary"
|
||||
title="Toggle value"
|
||||
@click="itemToggle(item)"
|
||||
>{{ item.value ? "mdi-checkbox-blank-outline" : "mdi-checkbox-marked-outline" }}</v-icon>
|
||||
<v-icon v-if="item.type == 'string' || item.type == 'number'"
|
||||
small
|
||||
right
|
||||
outline
|
||||
color="primary"
|
||||
title="Edit value"
|
||||
@click="itemAddDialog(item, true)"
|
||||
>mdi-pencil-outline</v-icon>
|
||||
<v-icon
|
||||
small
|
||||
right
|
||||
outlined
|
||||
color="red"
|
||||
title="Delete"
|
||||
:disabled="item.id == rootId"
|
||||
@click="itemDelete(item)"
|
||||
>mdi-minus</v-icon>
|
||||
</template>
|
||||
</template>
|
||||
</v-treeview>
|
||||
<dougal-json-builder-property-dialog
|
||||
:open="editor"
|
||||
v-model="edit"
|
||||
v-bind="editorProperties"
|
||||
@save="editorSave"
|
||||
@close="editorClose"
|
||||
></dougal-json-builder-property-dialog>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepValue, deepSet } from '@/lib/utils';
|
||||
import DougalJsonBuilderPropertyDialog from './property-dialog';
|
||||
|
||||
export default {
|
||||
name: "DougalJsonBuilder",
|
||||
|
||||
components: {
|
||||
DougalJsonBuilderPropertyDialog
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
name: String,
|
||||
sort: String
|
||||
},
|
||||
|
||||
data () {
|
||||
const rootId = Symbol("rootNode");
|
||||
return {
|
||||
rootId,
|
||||
active: [],
|
||||
open: [ rootId ],
|
||||
editor: false,
|
||||
editorProperties: {
|
||||
nameShown: true,
|
||||
nameEditable: true,
|
||||
typeShown: true,
|
||||
typeEditable: true,
|
||||
valueShown: true,
|
||||
serialisable: true
|
||||
},
|
||||
onEditorSave: (evt) => {},
|
||||
edit: {
|
||||
name: null,
|
||||
type: null,
|
||||
value: null
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
treeview () {
|
||||
|
||||
function sorter (key) {
|
||||
return function λ (a, b) {
|
||||
return a?.[key] > b?.[key]
|
||||
? 1
|
||||
: a?.[key] < b?.[key]
|
||||
? -1
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
function getType (value) {
|
||||
const t = typeof value;
|
||||
switch (t) {
|
||||
case "symbol":
|
||||
case "string":
|
||||
case "bigint":
|
||||
case "number":
|
||||
case "boolean":
|
||||
case "undefined":
|
||||
return t;
|
||||
case "object":
|
||||
return value === null
|
||||
? "null"
|
||||
: Array.isArray(value)
|
||||
? "array"
|
||||
: t;
|
||||
}
|
||||
}
|
||||
|
||||
function getIcon (type) {
|
||||
switch (type) {
|
||||
case "symbol":
|
||||
return "mdi-symbol";
|
||||
case "string":
|
||||
return "mdi-format-text";
|
||||
case "bigint":
|
||||
return "mdi-numeric";
|
||||
case "number":
|
||||
return "mdi-numeric";
|
||||
case "boolean":
|
||||
return "mdi-checkbox-intermediate-variant";
|
||||
case "undefined":
|
||||
return "mdi-border-none-variant";
|
||||
case "null":
|
||||
return "mdi-null";
|
||||
case "array":
|
||||
return "mdi-list-box-outline";
|
||||
case "object":
|
||||
return "mdi-format-list-bulleted-type";
|
||||
}
|
||||
return "mdi-help";
|
||||
}
|
||||
|
||||
const leaf = ([key, value], parent) => {
|
||||
const id = parent
|
||||
? parent.id+"."+key
|
||||
: key;
|
||||
const name = key;
|
||||
const type = getType(value);
|
||||
const icon = getIcon(type);
|
||||
const isArrayItem = parent?.type == "array";
|
||||
|
||||
const obj = {
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
icon,
|
||||
isArrayItem,
|
||||
};
|
||||
|
||||
if (parent) {
|
||||
obj.path = [...parent.path, key];
|
||||
} else {
|
||||
obj.path = [ key ];
|
||||
}
|
||||
|
||||
if (type == "object" || type == "array") {
|
||||
const children = [];
|
||||
for (const child of Object.entries(value)) {
|
||||
children.push(leaf(child, obj));
|
||||
}
|
||||
if (this.sort) {
|
||||
children.sort(sorter(this.sort));
|
||||
}
|
||||
obj.children = children;
|
||||
} else {
|
||||
obj.leaf = true;
|
||||
obj.value = value;
|
||||
/*
|
||||
obj.children = [{
|
||||
id: id+".value",
|
||||
name: String(value)
|
||||
}]
|
||||
*/
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
const rootNode = {
|
||||
id: this.rootId,
|
||||
name: this.name,
|
||||
type: getType(this.value),
|
||||
icon: getIcon(getType(this.value)),
|
||||
children: []
|
||||
};
|
||||
const view = [rootNode];
|
||||
|
||||
if (this.value) {
|
||||
for (const child of Object.entries(this.value)) {
|
||||
rootNode.children.push(leaf(child));
|
||||
}
|
||||
if (this.sort) {
|
||||
rootNode.children.sort(sorter(this.sort));
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
treeview () {
|
||||
if (!this.open.includes(this.rootId)) {
|
||||
this.open.push(this.rootId);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
openAll (open = true) {
|
||||
const walk = (obj) => {
|
||||
if (obj?.children) {
|
||||
for (const child of obj.children) {
|
||||
walk(child);
|
||||
}
|
||||
if (obj?.id) {
|
||||
this.open.push(obj.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const item of this.treeview) {
|
||||
walk (item);
|
||||
}
|
||||
},
|
||||
|
||||
itemDelete (item) {
|
||||
const parents = [...item.path];
|
||||
const key = parents.pop();
|
||||
|
||||
if (key) {
|
||||
|
||||
const value = structuredClone(this.value);
|
||||
const obj = parents.length ? deepValue(value, parents) : value;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
obj.splice(key, 1);
|
||||
} else {
|
||||
delete obj[key];
|
||||
}
|
||||
|
||||
this.$emit("input", value);
|
||||
|
||||
} else {
|
||||
|
||||
this.$emit("input", {});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
itemToggle (item, state) {
|
||||
const parents = [...item.path];
|
||||
const value = structuredClone(this.value);
|
||||
|
||||
if (parents.length) {
|
||||
deepSet(value, parents, state ?? !item.value)
|
||||
} else {
|
||||
value = state ?? !item.value;
|
||||
}
|
||||
|
||||
this.$emit("input", value);
|
||||
},
|
||||
|
||||
itemSet (path, content) {
|
||||
const parents = [...(path??[])];
|
||||
const key = parents.pop();
|
||||
|
||||
if (key !== undefined) {
|
||||
|
||||
const value = structuredClone(this.value);
|
||||
const obj = parents.length ? deepValue(value, parents) : value;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
if (key === null) {
|
||||
obj.push(content);
|
||||
} else {
|
||||
obj[key] = content;
|
||||
}
|
||||
} else {
|
||||
obj[key] = content;
|
||||
}
|
||||
|
||||
this.$emit("input", value);
|
||||
|
||||
} else {
|
||||
this.$emit("input", content);
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
itemAdd (path, content) {
|
||||
let value = structuredClone(this.value);
|
||||
let path_ = [...(path??[])];
|
||||
|
||||
if (path_.length) {
|
||||
try {
|
||||
deepSet(value, path_, content);
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
this.itemSet(path, content);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value = content;
|
||||
}
|
||||
|
||||
this.$emit("input", value);
|
||||
},
|
||||
|
||||
itemAddDialog (item, edit=false) {
|
||||
|
||||
if (!this.open.includes(item.id)) {
|
||||
this.open.push(item.id);
|
||||
}
|
||||
|
||||
if (edit) {
|
||||
this.editorReset({
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
value: item.value
|
||||
}, {nameEditable: false});
|
||||
} else {
|
||||
this.editorReset({}, {
|
||||
nameShown: item.type != "array",
|
||||
nameRequired: item.type != "array"
|
||||
});
|
||||
}
|
||||
|
||||
this.onEditorSave = (evt) => {
|
||||
this.editor = false;
|
||||
|
||||
let transformer;
|
||||
switch(this.edit.type) {
|
||||
case "symbol":
|
||||
transformer = Symbol;
|
||||
break;
|
||||
case "string":
|
||||
transformer = String;
|
||||
break;
|
||||
case "bigint":
|
||||
transformer = BigInt;
|
||||
break;
|
||||
case "number":
|
||||
transformer = Number;
|
||||
break;
|
||||
case "boolean":
|
||||
transformer = Boolean;
|
||||
break;
|
||||
case "undefined":
|
||||
transformer = () => { return undefined; };
|
||||
break;
|
||||
case "object":
|
||||
transformer = (v) =>
|
||||
typeof v == "object"
|
||||
? v
|
||||
: (typeof v == "string" && v.length)
|
||||
? JSON.parse(v)
|
||||
: {};
|
||||
break;
|
||||
case "null":
|
||||
transformer = () => null;
|
||||
break;
|
||||
case "array":
|
||||
// FIXME not great
|
||||
transformer = (v) =>
|
||||
Array.isArray(v)
|
||||
? v
|
||||
: [];
|
||||
break;
|
||||
}
|
||||
|
||||
const value = transformer(this.edit.value);
|
||||
|
||||
const path = [...(item.path??[])];
|
||||
|
||||
if (!edit) {
|
||||
if (item.type == "array") {
|
||||
path.push(null);
|
||||
} else {
|
||||
path.push(this.edit.name);
|
||||
}
|
||||
}
|
||||
this.itemAdd(path, value);
|
||||
};
|
||||
this.editor = true;
|
||||
|
||||
},
|
||||
|
||||
XXitemEditDialog (item) {
|
||||
|
||||
this.editorReset({
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
value: item.value}, {nameEditable: false});
|
||||
|
||||
this.onEditorSave = (evt) => {
|
||||
this.editor = false;
|
||||
|
||||
let transformer;
|
||||
switch(this.edit.type) {
|
||||
case "symbol":
|
||||
transformer = Symbol;
|
||||
break;
|
||||
case "string":
|
||||
transformer = String;
|
||||
break;
|
||||
case "bigint":
|
||||
transformer = BigInt;
|
||||
break;
|
||||
case "number":
|
||||
transformer = Number;
|
||||
break;
|
||||
case "boolean":
|
||||
transformer = Boolean;
|
||||
break;
|
||||
case "undefined":
|
||||
transformer = () => { return undefined; };
|
||||
break;
|
||||
case "object":
|
||||
transformer = (v) =>
|
||||
typeof v == "object"
|
||||
? v
|
||||
: (typeof v == "string" && v.length)
|
||||
? JSON.parse(v)
|
||||
: {};
|
||||
break;
|
||||
case "null":
|
||||
transformer = () => null;
|
||||
break;
|
||||
case "array":
|
||||
// FIXME not great
|
||||
transformer = (v) =>
|
||||
Array.isArray(v)
|
||||
? v
|
||||
: [];
|
||||
break;
|
||||
}
|
||||
|
||||
const key = this.edit.name;
|
||||
const value = transformer(this.edit.value);
|
||||
this.itemAdd(item, key, value);
|
||||
}
|
||||
this.editor = true;
|
||||
|
||||
},
|
||||
|
||||
editorReset (values, props) {
|
||||
this.edit = {
|
||||
name: values?.name,
|
||||
type: values?.type,
|
||||
value: values?.value
|
||||
};
|
||||
|
||||
this.editorProperties = {
|
||||
nameShown: props?.nameShown ?? true,
|
||||
nameEditable: props?.nameEditable ?? true,
|
||||
nameRequired: props?.nameRequired ?? true,
|
||||
typeShown: props?.typeShown ?? true,
|
||||
typeEditable: props?.typeEditable ?? true,
|
||||
valueShown: props?.valueShown ?? true,
|
||||
serialisable: props?.serialisable ?? true
|
||||
};
|
||||
},
|
||||
|
||||
editorSave (evt) {
|
||||
this.onEditorSave?.(evt);
|
||||
},
|
||||
|
||||
editorClose () {
|
||||
this.editor = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<v-dialog :value="open" @input="$emit('close')">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
|
||||
<v-text-field v-if="nameShown"
|
||||
label="Name"
|
||||
:disabled="!nameEditable"
|
||||
v-model.sync="value.name"
|
||||
></v-text-field>
|
||||
|
||||
<v-select v-if="typeShown"
|
||||
label="Type"
|
||||
:items="types"
|
||||
:disabled="!typeEditable"
|
||||
v-model.sync="value.type"
|
||||
></v-select>
|
||||
|
||||
<template v-if="valueShown">
|
||||
<v-text-field v-if="value.type == 'number' || value.type == 'bigint'"
|
||||
label="Value"
|
||||
type="number"
|
||||
v-model.sync="value.value"
|
||||
></v-text-field>
|
||||
|
||||
<v-textarea v-else-if="value.type == 'string'"
|
||||
label="Value"
|
||||
v-model.sync="value.value"
|
||||
></v-textarea>
|
||||
|
||||
<v-radio-group v-else-if="value.type == 'boolean'"
|
||||
v-model.sync="value.value"
|
||||
>
|
||||
<v-radio
|
||||
label="true"
|
||||
:value="true"
|
||||
></v-radio>
|
||||
<v-radio
|
||||
label="false"
|
||||
:value="false"
|
||||
></v-radio>
|
||||
</v-radio-group>
|
||||
</template>
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
:disabled="!canSave"
|
||||
@click="$emit('save')"
|
||||
>Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "DougalJsonBuilderPropertyDialog",
|
||||
|
||||
props: {
|
||||
open: Boolean,
|
||||
value: Object,
|
||||
nameRequired: {type: Boolean, default: true},
|
||||
nameEditable: Boolean,
|
||||
nameShown: {type: Boolean, default: true},
|
||||
typeEditable: Boolean,
|
||||
typeShown: {type: Boolean, default: true},
|
||||
valueShown: {type: Boolean, default: true},
|
||||
serialisable: {type: Boolean, default: true},
|
||||
allowedTypes: Array
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
//key: null,
|
||||
//type: null,
|
||||
allTypes: [
|
||||
"symbol",
|
||||
"string",
|
||||
"bigint",
|
||||
"number",
|
||||
"boolean",
|
||||
"undefined",
|
||||
"object",
|
||||
"null",
|
||||
"array"
|
||||
],
|
||||
serialisableTypes: [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"object",
|
||||
"null",
|
||||
"array"
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
types () {
|
||||
return this.allowedTypes
|
||||
? this.allowedTypes
|
||||
: this.serialisable
|
||||
? this.serialisableTypes
|
||||
: this.allTypes;
|
||||
},
|
||||
|
||||
canSave () {
|
||||
return this.value.type && (this.value.name || this.nameRequired === false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -32,16 +32,61 @@
|
||||
</template>
|
||||
|
||||
<v-list dense>
|
||||
<v-list-item :href="`/settings/equipment`">
|
||||
<v-list-item-title>Equipment list</v-list-item-title>
|
||||
<v-list-item href="/settings/equipment">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Equipment list</v-list-item-title>
|
||||
<v-list-item-subtitle>Manage the list of equipment reported in logs</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action><v-icon small>mdi-view-list</v-icon></v-list-item-action>
|
||||
</v-list-item>
|
||||
<template v-if="false">
|
||||
<v-divider></v-divider>
|
||||
<v-list-item href="/settings">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Local settings</v-list-item-title>
|
||||
<v-list-item-subtitle>Manage this vessel's configuration</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action><v-icon small>mdi-ferry</v-icon></v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
</v-menu>
|
||||
|
||||
|
||||
<v-breadcrumbs :items="path"></v-breadcrumbs>
|
||||
<v-breadcrumbs :items="path">
|
||||
<template v-slot:item="{ item }">
|
||||
<v-breadcrumbs-item :href="item.href" :disabled="item.disabled" v-if="item.organisations">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span v-bind="attrs" v-on="on">{{ item.text }}</span>
|
||||
</template>
|
||||
<div class="text-overline">Project permissions</div>
|
||||
<v-simple-table dense>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Organisation</th><th>Read</th><th>Write</th><th>Edit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(operations, name) in item.organisations">
|
||||
<td v-if="name == '*'"><v-chip small label color="primary">All</v-chip></td>
|
||||
<td v-else><v-chip small label outlined>{{ name }}</v-chip></td>
|
||||
<td>{{ operations.read ? "✔" : " " }}</td>
|
||||
<td>{{ operations.write ? "✔" : " " }}</td>
|
||||
<td>{{ operations.edit ? "✔" : " " }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-tooltip>
|
||||
</v-breadcrumbs-item>
|
||||
<v-breadcrumbs-item :href="item.href" :disabled="item.disabled" v-else>
|
||||
{{ item.text }}
|
||||
</v-breadcrumbs-item>
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
|
||||
<template v-if="$route.name != 'Login'">
|
||||
<v-btn text link to="/login" v-if="!user && !loading">Log in</v-btn>
|
||||
@@ -50,10 +95,37 @@
|
||||
<v-menu
|
||||
offset-y
|
||||
>
|
||||
<template v-slot:activator="{on, attrs}">
|
||||
<v-avatar :color="user.colour || 'primary'" :title="`${user.name} (${user.role})`" v-bind="attrs" v-on="on">
|
||||
<span class="white--text">{{user.name.slice(0, 5)}}</span>
|
||||
</v-avatar>
|
||||
<template v-slot:activator="{ on: menu, attrs }">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on: tooltip }">
|
||||
<v-avatar :color="user.colour || 'primary'" v-bind="attrs" v-on="{...tooltip, ...menu}">
|
||||
<span class="white--text">{{user.name.slice(0, 5)}}</span>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<div class="text-overline">{{ user.name }}</div>
|
||||
<v-card flat class="my-1" v-if="user.description">
|
||||
<v-card-text class="pb-1" v-html="$root.markdown(user.description)">
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-simple-table dense>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Organisation</th><th>Read</th><th>Write</th><th>Edit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="org in user.organisations">
|
||||
<td v-if="org.name == '*'"><v-chip small label color="primary">All</v-chip></td>
|
||||
<td v-else><v-chip small label outlined>{{ org.name }}</v-chip></td>
|
||||
<td>{{ org.operations.read ? "✔" : " " }}</td>
|
||||
<td>{{ org.operations.write ? "✔" : " " }}</td>
|
||||
<td>{{ org.operations.edit ? "✔" : " " }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<v-list dense>
|
||||
@@ -66,8 +138,29 @@
|
||||
</v-list-item>
|
||||
<v-list-item link to="/logout" v-else>
|
||||
<v-list-item-icon><v-icon small>mdi-logout</v-icon></v-list-item-icon>
|
||||
<v-list-item-title>Log out</v-list-item-title>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Log out</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<template v-if="canManageUsers">
|
||||
<v-list-item link to="/users">
|
||||
<v-list-item-icon><v-icon small>mdi-account-multiple</v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Manage users</v-list-item-title>
|
||||
<v-list-item-subtitle>Add, edit and remove users</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-else-if="user && !user.autologin">
|
||||
<v-list-item link :to="`/users/${user.id}`">
|
||||
<v-list-item-icon><v-icon small>mdi-account</v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>User profile</v-list-item-title>
|
||||
<v-list-item-subtitle>Edit your user profile</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
</v-menu>
|
||||
@@ -102,6 +195,19 @@ export default {
|
||||
.pop()?.component;
|
||||
},
|
||||
|
||||
title () {
|
||||
return this.user.name + "\n" + [...this.user.organisations].map( ({name, operations}) => {
|
||||
if (name == "*") name = "All organisations";
|
||||
let str = name+": ";
|
||||
str += [ "read", "write", "edit" ].map( op => operations[op] ? op : null ).filter( op => op ).join(", ");
|
||||
return str;
|
||||
}).join("\n")
|
||||
},
|
||||
|
||||
canManageUsers () {
|
||||
return this.user.organisations.accessToOperation("edit").length;
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'loading'])
|
||||
},
|
||||
|
||||
|
||||
112
lib/www/client/source/src/components/organisations-item.vue
Normal file
112
lib/www/client/source/src/components/organisations-item.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<v-row dense no-gutters>
|
||||
|
||||
<v-col>
|
||||
<slot name="prepend"></slot>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
class="mr-5"
|
||||
dense
|
||||
label="Name"
|
||||
:value="name"
|
||||
:readonly="true"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
class="mr-3"
|
||||
label="Read"
|
||||
v-model="operations.read"
|
||||
:readonly="readonly"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
class="mr-3"
|
||||
label="Write"
|
||||
v-model="operations.write"
|
||||
:readonly="readonly"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
class="mr-3"
|
||||
label="Edit"
|
||||
v-model="operations.edit"
|
||||
:readonly="readonly"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<!-- Just to fill the twelve-column grid -->
|
||||
<!--
|
||||
NOTE: this column could also be used for
|
||||
a popdown menu with additional operations
|
||||
if needed.
|
||||
-->
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<slot name="append"></slot>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { Organisations } from '@dougal/organisations';
|
||||
|
||||
export default {
|
||||
name: "DougalOrganisationsItem",
|
||||
|
||||
props: {
|
||||
name: String,
|
||||
value: Object,
|
||||
readonly: Boolean,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
operations: {...this.value}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: {
|
||||
handler (newValue) {
|
||||
this.operations = {...this.value};
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
operations: {
|
||||
handler (newValue) {
|
||||
if (["read", "write", "edit"].some( k => newValue[k] != this.value[k] )) {
|
||||
// Only emit if a value has actually changed
|
||||
this.$emit("input", {...newValue});
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
reset () {
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
191
lib/www/client/source/src/components/organisations.vue
Normal file
191
lib/www/client/source/src/components/organisations.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Organisations</v-card-title>
|
||||
<v-card-subtitle>Organisation access</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
|
||||
<v-container>
|
||||
|
||||
<dougal-organisations-item v-for="organisation in localOrganisations.names()"
|
||||
:key="organisation"
|
||||
:name="organisation"
|
||||
:value="localOrganisations.get(organisation)"
|
||||
@input="setOrganisation(organisation, $event)"
|
||||
>
|
||||
<template v-slot:append v-if="!readonly">
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Remove this organisation"
|
||||
>
|
||||
<v-icon
|
||||
color="error"
|
||||
@click="removeOrganisation(organisation)"
|
||||
>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dougal-organisations-item>
|
||||
|
||||
|
||||
<v-row no-gutters class="mb-2" v-if="!readonly">
|
||||
<h4>Add organisation</h4>
|
||||
</v-row>
|
||||
|
||||
<v-row no-gutters class="mb-2" v-if="!readonly">
|
||||
<v-combobox v-if="canCreateOrganisations"
|
||||
label="Organisation"
|
||||
:items="remainingOrganisations"
|
||||
v-model="organisationName"
|
||||
@input.native="organisationName = $event.srcElement.value"
|
||||
@keyup.enter="addOrganisation()"
|
||||
></v-combobox>
|
||||
<v-select v-else
|
||||
label="Organisation"
|
||||
:items="remainingOrganisations"
|
||||
v-model="organisationName"
|
||||
></v-select>
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Add organisation"
|
||||
:disabled="!(organisationName && organisationName.length)"
|
||||
@click="addOrganisation()"
|
||||
>
|
||||
<v-icon
|
||||
color="primary"
|
||||
>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<slot name="actions" v-bind="{ self, organisations, readonly, validationErrors, canCreateOrganisations }">
|
||||
</slot>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Organisations } from '@dougal/organisations';
|
||||
import DougalOrganisationsItem from './organisations-item';
|
||||
|
||||
|
||||
export default {
|
||||
name: "DougalOrganisations",
|
||||
|
||||
components: {
|
||||
DougalOrganisationsItem
|
||||
},
|
||||
|
||||
props: {
|
||||
self: Object,
|
||||
organisations: Object,
|
||||
readonly: Boolean
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
organisationName: "",
|
||||
localOrganisations: this.setLocalOrganisations(this.organisations)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
availableOrganisations () {
|
||||
return this.self.organisations.names();
|
||||
},
|
||||
|
||||
// Organisations available to add.
|
||||
// These are the organisations in `availableOrganisations`
|
||||
// minus any that have already been added.
|
||||
// The special value "*" (meaning "every organisation")
|
||||
// is not included.
|
||||
remainingOrganisations () {
|
||||
const orgs = [];
|
||||
|
||||
for (const org of this.availableOrganisations) {
|
||||
if (org != "*" && !this.localOrganisations.has(org)) {
|
||||
orgs.push(org);
|
||||
}
|
||||
}
|
||||
|
||||
return orgs;
|
||||
},
|
||||
|
||||
canCreateOrganisations () {
|
||||
return this.self.organisations.value("*")?.edit ?? false;
|
||||
},
|
||||
|
||||
validationErrors () {
|
||||
const errors = [];
|
||||
|
||||
// Check if there is at least one organisation
|
||||
if (this.localOrganisations.length) {
|
||||
errors.push("ERR_NO_ORGS");
|
||||
}
|
||||
|
||||
// Check if at least one organisation has edit rights
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
organisations (newValue) {
|
||||
this.localOrganisations = this.setLocalOrganisations(newValue);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
setLocalOrganisations (value) {
|
||||
return new Organisations(this.organisations);
|
||||
},
|
||||
|
||||
setOrganisation(name, value) {
|
||||
this.localOrganisations.set(name, value);
|
||||
this.$emit("update:organisations", new Organisations(this.localOrganisations));
|
||||
},
|
||||
|
||||
addOrganisation () {
|
||||
const key = this.organisationName;
|
||||
if (!this.localOrganisations.has(key)) {
|
||||
this.localOrganisations.set(key);
|
||||
this.$emit("update:organisations", this.localOrganisations);
|
||||
}
|
||||
this.organisationName = "";
|
||||
},
|
||||
|
||||
removeOrganisation (key) {
|
||||
if (this.localOrganisations.has(key)) {
|
||||
this.localOrganisations.remove(key);
|
||||
}
|
||||
this.$emit("update:organisations", this.localOrganisations);
|
||||
},
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
130
lib/www/client/source/src/components/project-settings/asaqc.vue
Normal file
130
lib/www/client/source/src/components/project-settings/asaqc.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>ASAQC</v-card-title>
|
||||
<v-card-subtitle>Equinor's cloud API configuration.</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="ASAQC ID"
|
||||
hint="ID number for this survey in ASAQC"
|
||||
persistent-hint
|
||||
type="number"
|
||||
v-model.number="cwo.id"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="IMO"
|
||||
hint="Project vessel's International Maritime Organisation's identification number"
|
||||
persistent-hint
|
||||
v-model.number="cwo.imo"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="MMSI"
|
||||
hint="Maritime Mobile Service Identities (MMSI) number"
|
||||
persistent-hint
|
||||
v-model.number="cwo.mmsi"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Subscription key"
|
||||
hint="Key to authenticate to ASAQC, provided by Equinor"
|
||||
persistent-hint
|
||||
:type="subscriptionKeyVisible ? 'text' : 'password'"
|
||||
:append-icon="subscriptionKeyVisible ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
v-model="cwo.subscriptionKey"
|
||||
@click:append="subscriptionKeyVisible = !subscriptionKeyVisible"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsASAQC",
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
subscriptionKeyVisible: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.cloud?.asaqc) {
|
||||
deepSet(this.value, [ "cloud", "asaqc" ], {
|
||||
id: null,
|
||||
imo: null,
|
||||
mmsi: null,
|
||||
subscriptionKey: null
|
||||
});
|
||||
}
|
||||
return this.value.cloud.asaqc;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "cloud", "asaqc" ], v);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Binning parameters</v-card-title>
|
||||
<!-- <v-card-subtitle></v-card-subtitle> -->
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="Azimuth"
|
||||
hint="Direction of I (inline) axis"
|
||||
persistent-hint
|
||||
suffix="°"
|
||||
type="number"
|
||||
v-model.number="cwo.theta"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="I increment"
|
||||
hint="Bin number increment along the inline axis"
|
||||
persistent-hint
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model.number="cwo.I_inc"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="J increment"
|
||||
hint="Bin number increment along the crossline axis"
|
||||
persistent-hint
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model.number="cwo.J_inc"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="I width"
|
||||
hint="Inline bin width (can be negative)"
|
||||
persistent-hint
|
||||
type="number"
|
||||
v-model.number="cwo.I_width"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="J width"
|
||||
hint="Crossline bin width (can be negative)"
|
||||
persistent-hint
|
||||
type="number"
|
||||
v-model.number="cwo.J_width"
|
||||
>
|
||||
</v-text-field>
|
||||
<fieldset class="pa-3 mt-3">
|
||||
<legend>Origin</legend>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
label="Easting"
|
||||
hint="Bin origin easting"
|
||||
persistent-hint
|
||||
type="number"
|
||||
v-model.number="cwo.origin.easting"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
label="Northing"
|
||||
hint="Bin origin northing"
|
||||
persistent-hint
|
||||
type="number"
|
||||
v-model.number="cwo.origin.northing"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
label="I"
|
||||
hint="Bin origin inline"
|
||||
persistent-hint
|
||||
type="number"
|
||||
step="1"
|
||||
v-model.number="cwo.origin.I"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
label="J"
|
||||
hint="Bin origin crossline"
|
||||
persistent-hint
|
||||
type="number"
|
||||
step="1"
|
||||
v-model.number="cwo.origin.J"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</fieldset>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsBinning",
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.binning) {
|
||||
deepSet(this.value, [ "binning" ], {
|
||||
theta: null,
|
||||
Iinc: 1,
|
||||
Jinc: 1,
|
||||
Iwidth: null,
|
||||
Jwidth: null,
|
||||
origin: {
|
||||
easting: null,
|
||||
northing: null,
|
||||
I: null,
|
||||
J: null
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.value.binning;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "binning" ], v);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title v-text="title"></v-card-title>
|
||||
<v-card-subtitle v-text="subtitle"></v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-tabs v-model="tab">
|
||||
<v-tab>Paths</v-tab>
|
||||
<v-tab>Globs</v-tab>
|
||||
<v-tab v-if="pattern">Pattern</v-tab>
|
||||
<v-tab v-if="lineNameInfo">Line info</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-items v-model="tab">
|
||||
|
||||
<v-tab-item>
|
||||
<v-card flat>
|
||||
<v-card-subtitle>
|
||||
A list of directories which are searched for matching files.
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<slot name="paths"></slot>
|
||||
<v-form>
|
||||
<v-text-field v-for="(item, index) in paths" :key="index"
|
||||
v-model="paths[index]"
|
||||
>
|
||||
<dougal-file-browser-dialog
|
||||
slot="append"
|
||||
v-model="paths[index]"
|
||||
:root="rootPath"
|
||||
:mimetypes="[ 'inode/directory' ]"
|
||||
title="Select a directory"
|
||||
></dougal-file-browser-dialog>
|
||||
<v-btn slot="append-outer"
|
||||
fab
|
||||
x-small text
|
||||
dark
|
||||
color="red"
|
||||
title="Remove"
|
||||
@click="() => paths.splice(index, 1)"
|
||||
>
|
||||
<v-icon>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</v-text-field>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
fab dark
|
||||
x-small text
|
||||
color="primary"
|
||||
title="Add path"
|
||||
@click="() => paths.push('')"
|
||||
>
|
||||
<v-icon dark>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item>
|
||||
<v-card flat>
|
||||
<v-card-subtitle>
|
||||
A list of <a href="https://en.wikipedia.org/wiki/Glob_(programming)" target="_blank">glob patterns</a> expanding to match the files of interest. Note that Linux is case-sensitive.
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<slot name="globs"></slot>
|
||||
<v-form>
|
||||
<v-text-field v-for="(item, index) in globs" :key="index"
|
||||
v-model="globs[index]"
|
||||
>
|
||||
<v-btn slot="append-outer"
|
||||
fab
|
||||
x-small text
|
||||
dark
|
||||
color="red"
|
||||
title="Remove"
|
||||
@click="() => globs.splice(index, 1)"
|
||||
>
|
||||
<v-icon>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</v-text-field>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
fab dark
|
||||
x-small text
|
||||
color="primary"
|
||||
title="Add glob pattern"
|
||||
@click="() => globs.push('')"
|
||||
>
|
||||
<v-icon dark>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item v-if="pattern">
|
||||
<v-card flat>
|
||||
<v-card-subtitle>
|
||||
Regular expression that describes the file format definition. Used to capture information such as line and sequence number, etc.
|
||||
<b v-if="lineNameInfo">Note: Use the <a @click.stop="tab=3">line info</a> tab preferentially.</b>
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<slot name="pattern"></slot>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
class="mb-5"
|
||||
label="Regular expression"
|
||||
v-model="pattern.regex"
|
||||
persistent-hint
|
||||
hint="Regular expression"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<fieldset class="pa-3 mb-5">
|
||||
<legend>Captures</legend>
|
||||
|
||||
<v-text-field v-for="(item, index) in pattern.captures" :key="index"
|
||||
v-model="pattern.captures[index]"
|
||||
>
|
||||
<v-btn slot="append-outer"
|
||||
fab
|
||||
x-small text
|
||||
dark
|
||||
color="red"
|
||||
title="Remove"
|
||||
@click="() => pattern.captures.splice(index, 1)"
|
||||
>
|
||||
<v-icon>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</v-text-field>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
fab dark
|
||||
x-small text
|
||||
color="primary"
|
||||
title="Add capture"
|
||||
@click="() => pattern.captures.push('')"
|
||||
>
|
||||
<v-icon dark>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</fieldset>
|
||||
|
||||
<v-text-field
|
||||
class="mb-5"
|
||||
label="Flags"
|
||||
v-model="pattern.flags"
|
||||
persistent-hint
|
||||
hint="Regular expression modifier flags"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item v-if="lineNameInfo">
|
||||
<v-card flat>
|
||||
<v-card-subtitle>
|
||||
Line information that will be extracted from file names
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<slot name="line-info"></slot>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="Example file name"
|
||||
hint="Enter the name of a representative file to make it easier to visualise your configuration"
|
||||
persistent-hint
|
||||
v-model="lineNameInfo.example"
|
||||
></v-text-field>
|
||||
|
||||
<dougal-fixed-string-decoder
|
||||
:multiline="true"
|
||||
:text="lineNameInfo.example"
|
||||
:fixed.sync="lineNameInfo.fixed"
|
||||
:fields.sync="lineNameInfo.fields"
|
||||
></dougal-fixed-string-decoder>
|
||||
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
</v-tabs-items>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!isValid"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sample {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { deepCompare } from '@/lib/utils';
|
||||
import DougalFileBrowserDialog from '@/components/file-browser/file-browser-dialog';
|
||||
import DougalFixedStringDecoder from '@/components/decoder/fixed-string-decoder';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsFileMatchingParameters",
|
||||
|
||||
components: {
|
||||
DougalFileBrowserDialog,
|
||||
DougalFixedStringDecoder
|
||||
},
|
||||
|
||||
props: {
|
||||
title: String,
|
||||
subtitle: String,
|
||||
isValid: { type: Boolean, default: true },
|
||||
save: Function,
|
||||
|
||||
rootPath: String,
|
||||
paths: Array,
|
||||
globs: Array,
|
||||
lineNameInfo: Object,
|
||||
pattern: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
tab: null,
|
||||
globs_: [],
|
||||
paths_: [],
|
||||
lineNameInfo_: {},
|
||||
pattern_: {
|
||||
flags: "i",
|
||||
regex: null,
|
||||
captures: []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
globs () {
|
||||
if (!deepCompare(this.globs, this.globs_)) {
|
||||
this.globs_ = this.globs;
|
||||
}
|
||||
},
|
||||
|
||||
paths () {
|
||||
if (!deepCompare(this.paths, this.paths_)) {
|
||||
this.paths_ = this.paths;
|
||||
}
|
||||
},
|
||||
|
||||
lineNameInfo () {
|
||||
if (!deepCompare(this.lineNameInfo, this.lineNameInfo_)) {
|
||||
this.lineNameInfo_ = this.lineNameInfo;
|
||||
}
|
||||
},
|
||||
|
||||
pattern () {
|
||||
if (!deepCompare(this.pattern, this.pattern_)) {
|
||||
this.pattern_ = this.pattern;
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
this.globs_ = this.globs;
|
||||
this.paths_ = this.paths;
|
||||
this.lineNameInfo_ = this.lineNameInfo;
|
||||
this.pattern_ = this.pattern;
|
||||
},
|
||||
|
||||
/*
|
||||
save () {
|
||||
this.$emit('input', this.data);
|
||||
},
|
||||
*/
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Files</v-card-title>
|
||||
<v-card-subtitle>File path configuration for this project.</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="Project folder"
|
||||
hint="Root file path for this project"
|
||||
persistent-hint
|
||||
v-model="cwo.rootPath"
|
||||
>
|
||||
<dougal-file-browser-dialog
|
||||
slot="append"
|
||||
v-model="cwo.rootPath"
|
||||
mimetypes="inode/directory"
|
||||
></dougal-file-browser-dialog>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DougalFileBrowserDialog from '@/components/file-browser/file-browser-dialog';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsFilePath",
|
||||
|
||||
components: { DougalFileBrowserDialog },
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
return this.value;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
this.value = v;
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Geodetics</v-card-title>
|
||||
<v-card-subtitle>Geodetic parameters.</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="EPSG code"
|
||||
hint="EPSG code of the project's coordinate reference system"
|
||||
persistent-hint
|
||||
type="number"
|
||||
v-model.number="cwo.epsg"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsGeodetics",
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
return this.value;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
this.value = v;
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
157
lib/www/client/source/src/components/project-settings/groups.vue
Normal file
157
lib/www/client/source/src/components/project-settings/groups.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Groups</v-card-title>
|
||||
<v-card-subtitle>For <abbr title="Permanent Reservoir Monitoring">PRM</abbr> and 4D operations, each project can be assigned to one or more groups sharing the same (or substantially the same) preplots.</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-combobox
|
||||
v-model="groups"
|
||||
:items="items"
|
||||
multiple
|
||||
:search-input.sync="search"
|
||||
:hide-no-data="!search"
|
||||
hide-selected
|
||||
chips
|
||||
hint="Project group(s). Type a value to create a new group (case sensitive)"
|
||||
persistent-hint
|
||||
>
|
||||
<template v-slot:no-data>
|
||||
<v-list-item>
|
||||
<small>New group: </small>
|
||||
<v-chip class="ml-3"
|
||||
:color="`grey lighten-2`"
|
||||
light
|
||||
label
|
||||
small
|
||||
>
|
||||
{{ search }}
|
||||
</v-chip>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-slot:selection="{ attrs, item, parent, selected }">
|
||||
<v-chip
|
||||
v-if="item === Object(item)"
|
||||
v-bind="attrs"
|
||||
:color="`${item.colour} lighten-3`"
|
||||
:input-value="selected"
|
||||
label
|
||||
small
|
||||
>
|
||||
<span class="pr-2">
|
||||
{{ item.text }}
|
||||
</span>
|
||||
<v-icon
|
||||
small
|
||||
@click="parent.selectItem(item)"
|
||||
>
|
||||
$delete
|
||||
</v-icon>
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item="{ index, item }">
|
||||
<v-chip
|
||||
:color="`${item.colour} lighten-3`"
|
||||
label
|
||||
small
|
||||
>{{ item.text }}</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { deepSet } from '@/lib/utils';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsGroups",
|
||||
|
||||
props: [ "value" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
search: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
colours () {
|
||||
return [ "green", "purple", "indigo", "cyan", "teal", "orange" ];
|
||||
},
|
||||
|
||||
nextColour () {
|
||||
// FIXME Fix colour when adding a new group
|
||||
return this.colours[(this.items.length + (this.value?.groups?.length ?? 0)) % this.colours.length];
|
||||
},
|
||||
|
||||
items () {
|
||||
return this.projectGroups.map((item, idx) => {
|
||||
return {
|
||||
text: item,
|
||||
colour: this.colours[idx % this.colours.length]
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
groups: {
|
||||
|
||||
get () {
|
||||
return this.value?.groups?.map(i => {
|
||||
return typeof i === "string"
|
||||
? { text: i, colour: this.nextColour}
|
||||
: i
|
||||
}) ?? [];
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
this.value.groups = v?.map( i => i?.text ?? i );
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
...mapGetters(["projectGroups"])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<dougal-project-settings-file-matching-parameters
|
||||
title="Final P1/11"
|
||||
subtitle="Final P1/11 files location and parameters."
|
||||
v-bind="{rootPath: value.rootPath}"
|
||||
v-bind.sync="bind"
|
||||
:is-valid="isValid"
|
||||
:save="save"
|
||||
>
|
||||
<template v-slot:paths v-if="validationErrors.includes('ERR_PATHS')">
|
||||
<v-alert type="warning">
|
||||
At least one path entry is required.<br/>
|
||||
<ul>
|
||||
<li>If you have final P1/11 files in multiple paths (e.g., each file in its own sequence directory), enter here the parent directory.</li>
|
||||
<li>If files are across multiple paths without a common ancestor, you must add multiple entries here.</li>
|
||||
</ul>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<template v-slot:globs v-if="validationErrors.includes('ERR_GLOBS')">
|
||||
<v-alert type="warning">
|
||||
At least one glob expression is required.
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<template v-slot:line-info v-if="validationErrors.includes('ERR_LINEINFO')">
|
||||
<v-alert type="warning">
|
||||
At least the following fields are required:
|
||||
<ul>
|
||||
<li><code>line</code> (integer, the preplot line number)</li>
|
||||
<li><code>sequence</code> (integer, the acquisition sequence number)</li>
|
||||
</ul>
|
||||
</v-alert>
|
||||
</template>
|
||||
</dougal-project-settings-file-matching-parameters>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sample {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
import DougalProjectSettingsFileMatchingParameters from '@/components/project-settings/file-matching-parameters';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsFinal111",
|
||||
|
||||
components: {
|
||||
DougalProjectSettingsFileMatchingParameters
|
||||
},
|
||||
|
||||
props: {
|
||||
title: String,
|
||||
subtitle: String,
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
tab: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.final?.p111) {
|
||||
deepSet(this.value, [ "final", "p111" ], {
|
||||
globs: [ "**/*.p111", "**/*.P111" ],
|
||||
paths: [],
|
||||
lineNameInfo: {
|
||||
example: "",
|
||||
fields: {
|
||||
line: {
|
||||
length: 4,
|
||||
type: "int"
|
||||
},
|
||||
sequence: {
|
||||
length: 3,
|
||||
type: "int"
|
||||
},
|
||||
incr: {
|
||||
length: 1,
|
||||
type: "bool"
|
||||
},
|
||||
attempt: {
|
||||
length: 1,
|
||||
type: "int"
|
||||
},
|
||||
file_no: {
|
||||
length: 3,
|
||||
type: "int"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.value.final.p111;
|
||||
} else {
|
||||
return {
|
||||
globs: [ "**/*.p111", "**/*.P111" ],
|
||||
paths: [],
|
||||
lineNameInfo: {
|
||||
example: "",
|
||||
fields: {
|
||||
line: {
|
||||
length: 4,
|
||||
type: "int"
|
||||
},
|
||||
sequence: {
|
||||
length: 3,
|
||||
type: "int"
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "final", "p111" ], v);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
bind () {
|
||||
return {
|
||||
globs: this.cwo?.globs,
|
||||
paths: this.cwo?.paths,
|
||||
pattern: this.cwo?.pattern,
|
||||
lineNameInfo: this.cwo?.lineNameInfo ?? {}
|
||||
};
|
||||
},
|
||||
|
||||
validationErrors () {
|
||||
const errors = [];
|
||||
|
||||
if (!this.cwo?.paths.length || !this.cwo?.paths[0].length) {
|
||||
// "Missing path: we need at least one directory where to search for matching files"
|
||||
errors.push("ERR_PATHS");
|
||||
}
|
||||
|
||||
if (!this.cwo?.globs.length) {
|
||||
// "Missing globs: we need at least one glob to search for matching files"
|
||||
errors.push("ERR_GLOBS");
|
||||
}
|
||||
|
||||
if (this.cwo?.lineNameInfo) {
|
||||
const pass = !this.cwo?.lineNameInfo?.fields || [ "line", "sequence" ].every( i =>
|
||||
["offset", "length"].every( j => j in (this.cwo?.lineNameInfo?.fields?.[i] ?? {}) ));
|
||||
|
||||
if (!pass) {
|
||||
// "Missing field info: We need at least 'line' and 'sequence' fields"
|
||||
errors.push("ERR_LINEINFO")
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
isValid () {
|
||||
return this.validationErrors.length == 0;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Pending sequence detection</v-card-title>
|
||||
<v-card-subtitle>Sequences which are pending acceptance (e.g., due to marginal quality) can be marked as such by the naming of their <b>final</b> files or parent directory. Dougal uses regular expression matching against the full path, not just the file name. This pattern applies to both P1/90 and P1/11 files.</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="Pattern"
|
||||
hint="Regular expression text"
|
||||
persistent-hint
|
||||
v-model="cwo.pattern.regex"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Flags"
|
||||
hint="Regular expression modifier flags"
|
||||
persistent-hint
|
||||
v-model="cwo.pattern.flags"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsFinalPending",
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.final?.pending.pattern) {
|
||||
deepSet(this.value, [ "final", "pending", "pattern" ], {
|
||||
flags: "i",
|
||||
regex: "PENDING"
|
||||
});
|
||||
}
|
||||
return this.value.final.pending;
|
||||
} else {
|
||||
return {
|
||||
flags: "i",
|
||||
regex: "PENDING"
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "final", "pending", "pattern" ], v);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>NTBP detection</v-card-title>
|
||||
<v-card-subtitle><abbr title="Not to be processed">NTBP</abbr> sequences are denoted by the naming of its files or any of its parent directories. Dougal uses regular expression matching against the full path, not just the file name. This pattern applies to both P1/90 and P1/11 files.</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="Pattern"
|
||||
hint="Regular expression text"
|
||||
persistent-hint
|
||||
v-model="cwo.pattern.regex"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Flags"
|
||||
hint="Regular expression modifier flags"
|
||||
persistent-hint
|
||||
v-model="cwo.pattern.flags"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsRawNTBP",
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.raw?.ntbp.pattern) {
|
||||
deepSet(this.value, [ "raw", "ntbp", "pattern" ], {
|
||||
flags: "i",
|
||||
regex: "NTBP"
|
||||
});
|
||||
}
|
||||
return this.value.raw.ntbp;
|
||||
} else {
|
||||
return {
|
||||
flags: "i",
|
||||
regex: "NTBP"
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "raw", "ntbp", "pattern" ], v);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<dougal-project-settings-file-matching-parameters
|
||||
title="Raw P1/11"
|
||||
subtitle="Raw P1/11 files location and parameters."
|
||||
v-bind="{rootPath: value.rootPath}"
|
||||
v-bind.sync="bind"
|
||||
:is-valid="isValid"
|
||||
:save="save"
|
||||
>
|
||||
<template v-slot:paths v-if="validationErrors.includes('ERR_PATHS')">
|
||||
<v-alert type="warning">
|
||||
At least one path entry is required.<br/>
|
||||
<ul>
|
||||
<li>If you have final P1/11 files in multiple paths (e.g., each file in its own sequence directory), enter here the parent directory.</li>
|
||||
<li>If files are across multiple paths without a common ancestor, you must add multiple entries here.</li>
|
||||
</ul>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<template v-slot:globs v-if="validationErrors.includes('ERR_GLOBS')">
|
||||
<v-alert type="warning">
|
||||
At least one glob expression is required.
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<template v-slot:line-info v-if="validationErrors.includes('ERR_LINEINFO')">
|
||||
<v-alert type="warning">
|
||||
At least the following fields are required:
|
||||
<ul>
|
||||
<li><code>line</code> (integer, the preplot line number)</li>
|
||||
<li><code>sequence</code> (integer, the acquisition sequence number)</li>
|
||||
</ul>
|
||||
</v-alert>
|
||||
</template>
|
||||
</dougal-project-settings-file-matching-parameters>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sample {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
import DougalProjectSettingsFileMatchingParameters from '@/components/project-settings/file-matching-parameters';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsRaw111",
|
||||
|
||||
components: {
|
||||
DougalProjectSettingsFileMatchingParameters
|
||||
},
|
||||
|
||||
props: {
|
||||
title: String,
|
||||
subtitle: String,
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
tab: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.raw?.p111) {
|
||||
deepSet(this.value, [ "raw", "p111" ], {
|
||||
globs: [ "**/*.p111", "**/*.P111" ],
|
||||
paths: [],
|
||||
lineNameInfo: {
|
||||
example: "",
|
||||
fields: {
|
||||
line: {
|
||||
length: 4,
|
||||
type: "int"
|
||||
},
|
||||
sequence: {
|
||||
length: 3,
|
||||
type: "int"
|
||||
},
|
||||
incr: {
|
||||
length: 1,
|
||||
type: "bool"
|
||||
},
|
||||
attempt: {
|
||||
length: 1,
|
||||
type: "int"
|
||||
},
|
||||
file_no: {
|
||||
length: 3,
|
||||
type: "int"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.value.raw.p111;
|
||||
} else {
|
||||
return {
|
||||
globs: [ "**/*.p111", "**/*.P111" ],
|
||||
paths: [],
|
||||
lineNameInfo: {
|
||||
example: "",
|
||||
fields: {
|
||||
line: {
|
||||
length: 4,
|
||||
type: "int"
|
||||
},
|
||||
sequence: {
|
||||
length: 3,
|
||||
type: "int"
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "raw", "p111" ], v);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
bind () {
|
||||
return {
|
||||
globs: this.cwo?.globs,
|
||||
paths: this.cwo?.paths,
|
||||
pattern: this.cwo?.pattern,
|
||||
lineNameInfo: this.cwo?.lineNameInfo
|
||||
};
|
||||
},
|
||||
|
||||
validationErrors () {
|
||||
const errors = [];
|
||||
|
||||
if (!this.cwo?.paths.length || !this.cwo?.paths[0].length) {
|
||||
// "Missing path: we need at least one directory where to search for matching files"
|
||||
errors.push("ERR_PATHS");
|
||||
}
|
||||
|
||||
if (!this.cwo?.globs.length) {
|
||||
// "Missing globs: we need at least one glob to search for matching files"
|
||||
errors.push("ERR_GLOBS");
|
||||
}
|
||||
|
||||
if (this.cwo?.lineNameInfo) {
|
||||
const pass = !this.cwo?.lineNameInfo?.fields || [ "line", "sequence" ].every( i =>
|
||||
["offset", "length"].every( j => j in (this.cwo?.lineNameInfo?.fields?.[i] ?? {}) ));
|
||||
|
||||
if (!pass) {
|
||||
// "Missing field info: We need at least 'line' and 'sequence' fields"
|
||||
errors.push("ERR_LINEINFO")
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
isValid () {
|
||||
return this.validationErrors.length == 0;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<dougal-project-settings-file-matching-parameters
|
||||
title="Smartsource header data"
|
||||
subtitle="Smartsource header data files location and parameters."
|
||||
v-bind="{rootPath: value.rootPath}"
|
||||
v-bind.sync="bind"
|
||||
:is-valid="isValid"
|
||||
:save="save"
|
||||
>
|
||||
</dougal-project-settings-file-matching-parameters>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sample {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
import DougalProjectSettingsFileMatchingParameters from '@/components/project-settings/file-matching-parameters';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsSmartsourceHeader",
|
||||
|
||||
components: {
|
||||
DougalProjectSettingsFileMatchingParameters
|
||||
},
|
||||
|
||||
props: {
|
||||
title: String,
|
||||
subtitle: String,
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
tab: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.raw?.source?.smsrc?.header) {
|
||||
deepSet(this.value, [ "raw", "source", "smsrc", "header" ], {
|
||||
globs: [ "**/*.hdr", "**/*.HDR" ],
|
||||
paths: [],
|
||||
lineNameInfo: {
|
||||
example: "",
|
||||
fields: {
|
||||
line: {
|
||||
length: 4,
|
||||
type: "int"
|
||||
},
|
||||
sequence: {
|
||||
length: 3,
|
||||
type: "int"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.value.raw.source.smsrc.header;
|
||||
} else {
|
||||
return {
|
||||
globs: [ "**/*.hdr", "**/*.HDR" ],
|
||||
paths: [],
|
||||
lineNameInfo: {
|
||||
example: "",
|
||||
fields: {
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "raw", "source", "smsrc", "header" ], v);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
bind () {
|
||||
return {
|
||||
globs: this.cwo?.globs,
|
||||
paths: this.cwo?.paths,
|
||||
pattern: this.cwo?.pattern,
|
||||
lineNameInfo: this.cwo?.lineNameInfo
|
||||
};
|
||||
},
|
||||
|
||||
isValid () {
|
||||
return !!(this.cwo?.paths.length && this.cwo?.globs.length && (
|
||||
this.cwo?.pattern?.regex &&
|
||||
["direction", "line", "sequence"].every( i => this.cwo?.pattern?.captures?.includes(i) )) || (
|
||||
this.cwo?.lineNameInfo &&
|
||||
this.cwo?.lineNameInfo?.fields &&
|
||||
[ "line", "sequence", "incr" ].every( i =>
|
||||
["offset", "length"].every( j => j in this.cwo?.lineNameInfo.fields[i] ))));
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<dougal-project-settings-file-matching-parameters
|
||||
title="Smartsource hydrophone data"
|
||||
subtitle="Smartsource SEG-Y hydrophone data files location and parameters."
|
||||
v-bind="{rootPath: value.rootPath}"
|
||||
v-bind.sync="bind"
|
||||
:is-valid="isValid"
|
||||
:save="save"
|
||||
>
|
||||
</dougal-project-settings-file-matching-parameters>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sample {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
import setIfDifferent from '@/lib/watcher-mixin';
|
||||
import DougalProjectSettingsFileMatchingParameters from '@/components/project-settings/file-matching-parameters';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsSmartsourceSegy",
|
||||
|
||||
components: {
|
||||
DougalProjectSettingsFileMatchingParameters
|
||||
},
|
||||
|
||||
props: {
|
||||
title: String,
|
||||
subtitle: String,
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
tab: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.raw?.source?.smsrc?.segy) {
|
||||
deepSet(this.value, [ "raw", "source", "smsrc", "segy" ], {
|
||||
globs: [ "**/*.hdr", "**/*.HDR" ],
|
||||
paths: [],
|
||||
lineNameInfo: {
|
||||
example: "",
|
||||
fields: {
|
||||
line: {
|
||||
length: 4,
|
||||
type: "int"
|
||||
},
|
||||
sequence: {
|
||||
length: 3,
|
||||
type: "int"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.value.raw.source.smsrc.segy;
|
||||
} else {
|
||||
return {
|
||||
globs: [ "**/*.hdr", "**/*.HDR" ],
|
||||
paths: [],
|
||||
lineNameInfo: {
|
||||
example: "",
|
||||
fields: {
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "raw", "source", "smsrc", "segy" ], v);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
bind () {
|
||||
return {
|
||||
globs: this.cwo?.globs,
|
||||
paths: this.cwo?.paths,
|
||||
pattern: this.cwo?.pattern,
|
||||
lineNameInfo: this.cwo?.lineNameInfo
|
||||
};
|
||||
},
|
||||
|
||||
isValid () {
|
||||
return !!(this.cwo?.paths.length && this.cwo?.globs.length && (
|
||||
this.cwo?.pattern?.regex &&
|
||||
["direction", "line", "sequence"].every( i => this.cwo?.pattern?.captures?.includes(i) )) || (
|
||||
this.cwo?.lineNameInfo &&
|
||||
this.cwo?.lineNameInfo?.fields &&
|
||||
[ "line", "sequence", "incr" ].every( i =>
|
||||
["offset", "length"].every( j => j in this.cwo?.lineNameInfo.fields[i] ))));
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Name, ID & geodetics</v-card-title>
|
||||
<v-card-subtitle>The survey's name, short ID and EPSG code. This is the minimum required to create a new survey. <b>These values cannot be changed once the project has been created!</b></v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="ID"
|
||||
hint="Short survey ID"
|
||||
persistent-hint
|
||||
v-model="id"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Name"
|
||||
hint="Survey name"
|
||||
persistent-hint
|
||||
v-model="name"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="EPSG code"
|
||||
hint="EPSG code of the project's coordinate reference system"
|
||||
persistent-hint
|
||||
type="number"
|
||||
v-model.number="epsg"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsNameIdGeodetics",
|
||||
|
||||
props: [ "value" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
id: "",
|
||||
name: "",
|
||||
epsg: null
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value (newValue) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
for (const key of Object.keys(this.$data)) {
|
||||
this[key] = this.value[key];
|
||||
}
|
||||
},
|
||||
|
||||
save () {
|
||||
this.$emit('input', {...this.$data});
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Files</v-card-title>
|
||||
<v-card-subtitle>File path configuration for this project.</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="ID"
|
||||
hint="Short survey ID"
|
||||
persistent-hint
|
||||
v-model="id"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Name"
|
||||
hint="Survey name"
|
||||
persistent-hint
|
||||
v-model="name"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Project folder"
|
||||
hint="Root file path for this project"
|
||||
persistent-hint
|
||||
v-model="path"
|
||||
>
|
||||
<dougal-file-browser-dialog
|
||||
slot="append"
|
||||
v-model="path"
|
||||
mimetypes="inode/directory"
|
||||
></dougal-file-browser-dialog>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DougalFileBrowserDialog from '@/components/file-browser/file-browser-dialog';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsNameIdRootpath",
|
||||
|
||||
components: { DougalFileBrowserDialog },
|
||||
|
||||
props: [ "value" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
id: "",
|
||||
name: "",
|
||||
path: ""
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value (newValue) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
for (const key of Object.keys(this.$data)) {
|
||||
this[key] = this.value[key];
|
||||
}
|
||||
},
|
||||
|
||||
save () {
|
||||
this.$emit('input', {...this.$data});
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Name & ID</v-card-title>
|
||||
<v-card-subtitle>The survey's name and short ID. The latter must match the value used in ASAQC, if applicable.</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="ID"
|
||||
hint="Short survey ID"
|
||||
persistent-hint
|
||||
v-model="cwo.id"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Name"
|
||||
hint="Survey name"
|
||||
persistent-hint
|
||||
v-model="cwo.name"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsNameId",
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
return this.value;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
this.value = v;
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Not implemented</v-card-title>
|
||||
<v-card-text>
|
||||
The code for this configuration section has not yet been implemented.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
disabled
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
disabled
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsNotImplemented",
|
||||
|
||||
props: [ "value" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value (newValue) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Online line name</v-card-title>
|
||||
<v-card-subtitle>Line name decoding configuration for real-time data</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<dougal-fixed-string-encoder
|
||||
title="Line name format"
|
||||
subtitle="Format of line names as configured in the navigation system"
|
||||
label="Example line name"
|
||||
hint="Visualise your line name configuration with example values"
|
||||
:multiline="true"
|
||||
:properties="properties"
|
||||
:fields.sync="fields_"
|
||||
:values.sync="values_"
|
||||
></dougal-fixed-string-encoder>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sample {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
import DougalFixedStringEncoder from '@/components/encoder/fixed-string-encoder';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsOnlineLineNameFormat",
|
||||
|
||||
components: {
|
||||
DougalFixedStringEncoder
|
||||
},
|
||||
|
||||
props: {
|
||||
fields: Array,
|
||||
values: Object,
|
||||
properties: Object,
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
fields_: {
|
||||
get () {
|
||||
return this.fields;
|
||||
},
|
||||
set (v) {
|
||||
this.$emit("update", structuredClone({values: this.values, fields: v}));
|
||||
}
|
||||
},
|
||||
|
||||
values_: {
|
||||
get () {
|
||||
return this.values;
|
||||
},
|
||||
set (v) {
|
||||
this.$emit("update", structuredClone({values: v, fields: this.fields}));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<dougal-organisations
|
||||
:self="user"
|
||||
:organisations.sync="organisations_"
|
||||
>
|
||||
<template v-slot:actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</template>
|
||||
</dougal-organisations>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import DougalOrganisations from '../organisations'
|
||||
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsOrganisations",
|
||||
|
||||
components: {
|
||||
DougalOrganisations
|
||||
},
|
||||
|
||||
props: {
|
||||
organisations: Object,
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
organisations_: {
|
||||
get () {
|
||||
return this.organisations;
|
||||
},
|
||||
|
||||
set (v) {
|
||||
this.$emit("input", {
|
||||
...this.value,
|
||||
organisations: v.toJSON()
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Planner settings</v-card-title>
|
||||
<v-card-subtitle>Default values when creating new sequences in the planner. These values can then be changed for each individual entry.</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="Default line change duration"
|
||||
suffix="min"
|
||||
type="number"
|
||||
hint="Expected line change time in minutes"
|
||||
persistent-hint
|
||||
v-model.number="cwo.defaultLineChangeDuration"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Default acquisition speed"
|
||||
suffix="kt"
|
||||
type="number"
|
||||
hint="Expected acquisition speed in knots"
|
||||
persistent-hint
|
||||
v-model.number="cwo.defaultAcquisitionSpeed"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Overlap before"
|
||||
type="number"
|
||||
hint="Default number of shots to overlap before the FGSP, for reshoots"
|
||||
persistent-hint
|
||||
v-model.number="cwo.overlapBefore"
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Overlap after"
|
||||
type="number"
|
||||
hint="Default number of shots to overlap after the LGSP, for reshoots"
|
||||
persistent-hint
|
||||
v-model.number="cwo.overlapAfter"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsPlanner",
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.planner) {
|
||||
deepSet(this.value, [ "planner" ], {
|
||||
defaultLineChangeDuration: null,
|
||||
defaultAcquisitionSpeed: null,
|
||||
overlapBefore: null,
|
||||
overlapAfter: null
|
||||
});
|
||||
}
|
||||
return this.value.planner;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "planner" ], v);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,548 @@
|
||||
<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"
|
||||
:append-outer-icon="fileClass == 'saillines' && fileType == 'x-sl+csv' ? 'mdi-help-circle-outline' : ''"
|
||||
>
|
||||
<template v-slot:append-outer="" v-if="fileClass == 'saillines' && fileType == 'x-sl+csv'">
|
||||
<v-menu :close-on-content-click="false" v-model="tooltip">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" title="Information on sailline CSV files" @click="tooltip = !tooltip"><v-icon>mdi-help-circle-outline</v-icon></v-btn>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>Saillines CSV format</v-card-title>
|
||||
<v-card-text>
|
||||
<p>
|
||||
The input CSV should have the following comma-separated fields:
|
||||
<dl>
|
||||
<dt><code>sail_line</code></dt> <dd>The vessel line number</dd>
|
||||
<dt><code>incr</code></dt> <dd><em>1</em> if this line is to be shot in the incrementing shot points direction, <em>0</em> or blank otherwise</dd>
|
||||
<dt><code>ntba</code></dt> <dd><em>1</em> if this line is not to be acquired</dd>
|
||||
<dt><code>remarks</code></dt> <dd>Any comments pertinent to the line. Supports <a target="_blank" href="https://commonmark.org/help/">Markdown</a>.</dd>
|
||||
<dt><code>meta.colour</code></dt> <dd>An <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value">HTML colour</a>. Changes the background colour of the line in Dougal's ‘Lines’ tab.</dd>
|
||||
<dt><code>source_line</code></dt> <dd>The source line number. This column should be repeated once per gun array.</dd>
|
||||
</dl>
|
||||
</p>
|
||||
<p>See an <a target="_blank" href="https://gitlab.com/-/snippets/4873650">example file</a> (<a title="Direct download" href="https://gitlab.com/-/snippets/4873650/raw/main/preplots-saillines-example.csv?inline=false"><v-icon dense small>mdi-paperclip</v-icon></a>)</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
</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 },
|
||||
],
|
||||
tooltip: false,
|
||||
};
|
||||
},
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Preplots</v-card-title>
|
||||
<v-card-subtitle>Preplot files location and format.</v-card-subtitle>
|
||||
<v-card-text>
|
||||
|
||||
<v-expansion-panels v-model="panel">
|
||||
<v-expansion-panel v-for="(preplot, idx) in cwo" :key="idx">
|
||||
<v-expansion-panel-header>
|
||||
{{ titleFor(preplot) }}
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<dougal-project-settings-preplots-preplot
|
||||
v-model="cwo[idx]"
|
||||
:root-path="value.rootPath"
|
||||
></dougal-project-settings-preplots-preplot>
|
||||
|
||||
<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-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!isValid"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sample {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import DougalProjectSettingsPreplotsPreplot from '@/components/project-settings/preplots-preplot';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsPreplots",
|
||||
|
||||
components: {
|
||||
DougalProjectSettingsPreplotsPreplot,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
panel: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.preplots) {
|
||||
deepSet(this.value, [ "preplots" ], []);
|
||||
}
|
||||
return this.value.preplots;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "preplots" ], v);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
lastPreplotIsValid () {
|
||||
return this.cwo.length == 0 || this.validPreplot(this.cwo[this.cwo.length-1]);
|
||||
},
|
||||
|
||||
isValid () {
|
||||
return this.cwo.every(preplot => this.validPreplot(preplot));
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
titleFor (preplot) {
|
||||
let str = "";
|
||||
|
||||
if (preplot?.path) {
|
||||
str += preplot.path;
|
||||
} else {
|
||||
const idx = this.cwo.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.cwo.find(i => i == preplot);
|
||||
if (idx != -1) {
|
||||
this.cwo.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
|
||||
newPreplot () {
|
||||
if (this.lastPreplotIsValid) {
|
||||
const preplot = {
|
||||
path: "",
|
||||
type: "",
|
||||
format: "",
|
||||
fields: {},
|
||||
};
|
||||
this.cwo.push(preplot);
|
||||
this.panel = this.cwo.length - 1;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Production settings</v-card-title>
|
||||
<v-card-subtitle></v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="Nominal line change duration"
|
||||
hint="Duration of the nominal elapsed time circling between lines, in minutes. If this time is exceeded between one sequence and the next, an entry will be made in the events log"
|
||||
persistent-hint
|
||||
suffix="min"
|
||||
type="number"
|
||||
v-model.number="cwo.nominalLineChangeDuration"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<!--
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
>Save</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsProduction",
|
||||
|
||||
props: {
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.production) {
|
||||
deepSet(this.value, [ "production" ], {
|
||||
nominalLineChangeDuration: null
|
||||
});
|
||||
}
|
||||
return this.value.production;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "production" ], v);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title v-text="title"></v-card-title>
|
||||
<v-card-subtitle v-text="subtitle"></v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
class="mb-5"
|
||||
label="Regular expression"
|
||||
v-model="regex"
|
||||
persistent-hint
|
||||
hint="Regular expression"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<fieldset class="pa-3 mb-5">
|
||||
<legend>Captures</legend>
|
||||
|
||||
<v-text-field v-for="(item, index) in captures" :key="index"
|
||||
v-model="captures[index]"
|
||||
>
|
||||
<v-btn slot="append-outer"
|
||||
fab
|
||||
x-small text
|
||||
dark
|
||||
color="red"
|
||||
title="Remove"
|
||||
@click="() => captures.splice(index, 1)"
|
||||
>
|
||||
<v-icon>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</v-text-field>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
fab dark
|
||||
x-small text
|
||||
color="primary"
|
||||
title="Add capture"
|
||||
@click="() => captures.push('')"
|
||||
>
|
||||
<v-icon dark>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</fieldset>
|
||||
|
||||
<v-text-field
|
||||
class="mb-5"
|
||||
label="Flags"
|
||||
v-model="flags"
|
||||
persistent-hint
|
||||
hint="Regular expression modifier flags"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sample {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DougalProjectSettingsRegexPatternCaptures",
|
||||
|
||||
props: [ "value", "title", "subtitle" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
regex: "",
|
||||
flags: "",
|
||||
captures: []
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value (newValue) {
|
||||
this.reset();
|
||||
},
|
||||
|
||||
regex () {
|
||||
this.save();
|
||||
},
|
||||
|
||||
flags () {
|
||||
this.save();
|
||||
},
|
||||
|
||||
captures () {
|
||||
this.save();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
|
||||
this.regex = this.value?.regex;
|
||||
this.flags = this.value?.flags;
|
||||
this.captures = this.value?.captures ?? [];
|
||||
},
|
||||
|
||||
save () {
|
||||
this.$emit('input', {regex: this.regex, flags: this.flags, captures: this.captures});
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
256
lib/www/client/source/src/components/user-settings.vue
Normal file
256
lib/www/client/source/src/components/user-settings.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
User {{ name }} <v-chip class="mx-3" small>{{id}}</v-chip>
|
||||
<v-chip v-if="self.id == value.id"
|
||||
small
|
||||
color="primary"
|
||||
>It's me!</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-subtitle>User settings</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<!--
|
||||
<v-text-field
|
||||
label="User ID"
|
||||
hint="Unique user ID (read-only)"
|
||||
persistent-hint
|
||||
readonly
|
||||
disabled
|
||||
v-model="id"
|
||||
>
|
||||
</v-text-field>
|
||||
-->
|
||||
|
||||
<v-switch
|
||||
dense
|
||||
label="Active"
|
||||
:title="(self.id == value.id) ? 'You cannot make yourself inactive' : active ? 'Make this user inactive' : 'Make this user active'"
|
||||
:disabled="self.id == value.id"
|
||||
v-model="active"
|
||||
></v-switch>
|
||||
|
||||
<label class="mr-3 pt-5">Colour
|
||||
<v-menu v-model="colourMenu"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
:title="colour"
|
||||
dense
|
||||
small
|
||||
icon
|
||||
v-on="on"
|
||||
><v-icon :color="colour">mdi-palette</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-color-picker
|
||||
dot-size="25"
|
||||
mode="hexa"
|
||||
swatches-max-height="200"
|
||||
v-model="colour"
|
||||
></v-color-picker>
|
||||
</v-menu>
|
||||
</label>
|
||||
|
||||
<v-text-field
|
||||
v-if="showIp || ip"
|
||||
label="IP address"
|
||||
hint="IP address or subnet specification for auto-login"
|
||||
v-model="ip"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-if="showHost || host"
|
||||
label="Host name"
|
||||
hint="Hostname (for auto-login)"
|
||||
v-model="host"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
label="Name"
|
||||
hint="User name"
|
||||
v-model="name"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-if="showPasswordField"
|
||||
:type="visiblePassword ? 'text' : 'password'"
|
||||
:append-icon="visiblePassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="visiblePassword = !visiblePassword"
|
||||
label="Password"
|
||||
hint="User password"
|
||||
v-model="password"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
label="Email"
|
||||
hint="Email address"
|
||||
v-model="email"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-textarea
|
||||
class="mb-5"
|
||||
label="Remarks"
|
||||
hint="User description (visible to the user)"
|
||||
auto-grow
|
||||
v-model="description"
|
||||
></v-textarea>
|
||||
|
||||
<dougal-organisations
|
||||
:self="self"
|
||||
:organisations.sync="organisations"
|
||||
></dougal-organisations>
|
||||
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<slot name="actions" v-bind="{ isValid, hasErrors, errors, dirty }"></slot>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { User } from '@/lib/user';
|
||||
|
||||
import DougalOrganisations from './organisations'
|
||||
|
||||
export default {
|
||||
name: "DougalUserSettings",
|
||||
|
||||
components: {
|
||||
DougalOrganisations
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
self: Object, // User calling the dialogue
|
||||
|
||||
// The next three props determine whether the
|
||||
// ip, host, and password fields are shown even
|
||||
// when null / empty. If non-null, those fields
|
||||
// are always shown
|
||||
showIp: { type: Boolean, default: false },
|
||||
showHost: { type: Boolean, default: false },
|
||||
showPassword: { type: Boolean, default: false },
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
colourMenu: null,
|
||||
visiblePassword: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
id () { return this.value.id },
|
||||
|
||||
ip: {
|
||||
get () { return this.value.ip },
|
||||
set (v) { this.input("ip", v) }
|
||||
},
|
||||
|
||||
host: {
|
||||
get () { return this.value.host },
|
||||
set (v) { this.input("host", v) }
|
||||
},
|
||||
|
||||
name: {
|
||||
get () { return this.value.name },
|
||||
set (v) { this.input("name", v) }
|
||||
},
|
||||
|
||||
password: {
|
||||
get () { return this.value.password },
|
||||
set (v) { this.input("password", v) }
|
||||
},
|
||||
|
||||
active: {
|
||||
get () { return this.value.active },
|
||||
set (v) { this.input("active", v) }
|
||||
},
|
||||
|
||||
email: {
|
||||
get () { return this.value.email },
|
||||
set (v) { this.input("email", v) }
|
||||
},
|
||||
|
||||
colour: {
|
||||
get () { return this.value.colour },
|
||||
set (v) { this.input("colour", v) }
|
||||
},
|
||||
|
||||
description: {
|
||||
get () { return this.value.description },
|
||||
set (v) { this.input("description", v) }
|
||||
},
|
||||
|
||||
organisations: {
|
||||
get () { return this.value.organisations },
|
||||
set (v) { this.input("organisations", v) }
|
||||
},
|
||||
|
||||
errors () {
|
||||
return this.value.errors;
|
||||
},
|
||||
|
||||
hasErrors () {
|
||||
return !this.isValid;
|
||||
},
|
||||
|
||||
isValid () {
|
||||
return this.value.isValid;
|
||||
},
|
||||
|
||||
dirty () {
|
||||
return this.value?.dirty ?? false;
|
||||
},
|
||||
|
||||
showPasswordField () {
|
||||
return this.password || (this.showPassword &&
|
||||
!(this.showIp || this.ip || this.showHost || this.host));
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
validationErrors () {
|
||||
this.$emit("update:errors", this.validationErrors);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
input (k, v) {
|
||||
const user = new User(this.value);
|
||||
user[k] = v;
|
||||
this.$emit("input", user);
|
||||
},
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
1
lib/www/client/source/src/lib/deepMerge.js
Symbolic link
1
lib/www/client/source/src/lib/deepMerge.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../server/lib/utils/deepMerge.js
|
||||
47
lib/www/client/source/src/lib/hsl.js
Normal file
47
lib/www/client/source/src/lib/hsl.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/** Return an HSL colour as a function of an input value
|
||||
* `str`.
|
||||
*
|
||||
* Consider using as getHSL.bind(this) in Vue components
|
||||
* in order to get access to the Vuetify theme configuration.
|
||||
*/
|
||||
function getHSL (str, saturation = 1, lightness = 0.25, offset = 0) {
|
||||
|
||||
function getHash (v) {
|
||||
if (typeof (v??false)[Symbol.iterator] != "function") {
|
||||
// Not an iterable, make it one
|
||||
v = String(v);
|
||||
}
|
||||
|
||||
return Math.abs([...v, ..." "].reduce( (acc, cur) => String(cur).charCodeAt(0) + ((acc << 5) - acc), 0 ));
|
||||
}
|
||||
|
||||
const h = (getHash(str) + offset) % 360;
|
||||
const s = saturation * 100;
|
||||
const l = this?.$vuetify?.theme?.isDark
|
||||
? (1-lightness) * 100
|
||||
: lightness * 100;
|
||||
|
||||
return {h, s, l};
|
||||
|
||||
}
|
||||
|
||||
/** Return a CSS hsl() or hsla() colour
|
||||
* representation as a function of an input value.
|
||||
*
|
||||
* Consider using as getHSLColourFor.bind(this) – See
|
||||
* note for getHSL() above.
|
||||
*/
|
||||
function getHSLColourFor (str, opacity = 1, saturation, lightness, offset) {
|
||||
const _getHSL = getHSL.bind(this);
|
||||
const {h, s, l} = _getHSL(str, saturation, lightness, offset);
|
||||
if (opacity == 1) {
|
||||
return `hsl(${h},${s}%,${l}%)`;
|
||||
} else {
|
||||
return `hsla(${h},${s}%,${l}%, ${opacity})`;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getHSL,
|
||||
getHSLColourFor
|
||||
}
|
||||
10
lib/www/client/source/src/lib/truncate-text.js
Normal file
10
lib/www/client/source/src/lib/truncate-text.js
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
function truncateText (text, length=20) {
|
||||
if (text?.length <= length) {
|
||||
return text;
|
||||
} else {
|
||||
return text.slice(0, length/2)+"…"+text.slice(-(length/2));
|
||||
}
|
||||
}
|
||||
|
||||
export default truncateText;
|
||||
97
lib/www/client/source/src/lib/user/User.js
Normal file
97
lib/www/client/source/src/lib/user/User.js
Normal file
@@ -0,0 +1,97 @@
|
||||
|
||||
import { User as BaseUser } from '@dougal/user';
|
||||
|
||||
class User extends BaseUser {
|
||||
|
||||
api // Instance of Vuex api method
|
||||
dirty // Whether the values have changed since last saved
|
||||
|
||||
constructor (data, client) {
|
||||
super (data);
|
||||
|
||||
if (client) {
|
||||
this.api = client;
|
||||
} else if (data instanceof User) {
|
||||
this.api = data.api;
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
this.on("changed", () => this.dirty = true);
|
||||
}
|
||||
|
||||
static async fromAPI (api, id) {
|
||||
if (id) {
|
||||
const url = `/user/${id}`;
|
||||
const res = await api([url]);
|
||||
return new User(res, api);
|
||||
} else {
|
||||
const url = `/user`;
|
||||
const res = await api([url]);
|
||||
return res?.map( row => new User(row, api) );
|
||||
}
|
||||
}
|
||||
|
||||
/** Save this user to the server
|
||||
*
|
||||
* If this is a new user, the `api` parameter must be
|
||||
* supplied and this will result in a `POST` request.
|
||||
* For an existing user coming from the database,
|
||||
* `this.api` will be used for a `PUT` request.
|
||||
*/
|
||||
async save (api) {
|
||||
if (this.api) {
|
||||
const url = `/user/${this.id}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "PUT",
|
||||
body: this.toJSON()
|
||||
};
|
||||
const res = await this.api([url, init]);
|
||||
if (res) {
|
||||
this.dirty = false;
|
||||
return new User(res, this.api);
|
||||
} else {
|
||||
// Something has gone wrong
|
||||
console.log("Something has gone wrong (PUT)");
|
||||
}
|
||||
} else if (api) {
|
||||
const url = `/user`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: this.toJSON()
|
||||
}
|
||||
const res = await api([url, init]);
|
||||
if (res) {
|
||||
return new User(res, api);
|
||||
} else {
|
||||
// Something has gone wrong
|
||||
console.log("Something has gone wrong (POST)");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Don't know how to save this user");
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete this user from the server
|
||||
*/
|
||||
async remove () {
|
||||
const url = `/user/${this.id}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "PUT",
|
||||
body: this.toJSON()
|
||||
};
|
||||
const res = await this.api([url, init]);
|
||||
console.log("remove RES", res);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default User;
|
||||
5
lib/www/client/source/src/lib/user/index.js
Normal file
5
lib/www/client/source/src/lib/user/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import User from './User'
|
||||
|
||||
export {
|
||||
User
|
||||
}
|
||||
@@ -93,25 +93,25 @@ function geometryAsString (item, opts = {}) {
|
||||
}
|
||||
|
||||
/** Extract preferences by prefix.
|
||||
*
|
||||
*
|
||||
* This function returns a lambda which, given
|
||||
* a key or a prefix, extracts the relevant
|
||||
* preferences from the designated preferences
|
||||
* store.
|
||||
*
|
||||
*
|
||||
* For instance, assume preferences = {
|
||||
* "a.b.c.d": 1,
|
||||
* "a.b.e.f": 2,
|
||||
* "g.h": 3
|
||||
* }
|
||||
*
|
||||
*
|
||||
* And λ = preferencesλ(preferences). Then:
|
||||
*
|
||||
*
|
||||
* λ("a.b") → { "a.b.c.d": 1, "a.b.e.f": 2 }
|
||||
* λ("a.b.e.f") → { "a.b.e.f": 2 }
|
||||
* λ("g.x", {"g.x.": 99}) → { "g.x.": 99 }
|
||||
* λ("a.c", {"g.x.": 99}) → { "g.x.": 99 }
|
||||
*
|
||||
*
|
||||
* Note from the last two examples that a default value
|
||||
* may be provided and will be returned if a key does
|
||||
* not exist or is not searched for.
|
||||
@@ -133,9 +133,109 @@ function preferencesλ (preferences) {
|
||||
|
||||
}
|
||||
|
||||
/** Compare two possibly complex values for
|
||||
* loose equality, going as deep as required in the
|
||||
* case of complex objects.
|
||||
*/
|
||||
function deepCompare (a, b) {
|
||||
if (typeof a == "object" && typeof b == "object") {
|
||||
return !Object.entries(a).some( ([k, v]) => !deepCompare(v, b[k])) &&
|
||||
!Object.entries(b).some( ([k, v]) => !deepCompare(v, a[k]));
|
||||
} else {
|
||||
return a == b;
|
||||
}
|
||||
}
|
||||
|
||||
/** Compare two possibly complex values for
|
||||
* strict equality.
|
||||
*/
|
||||
function deepEqual (a, b) {
|
||||
if (typeof a === "object" && typeof b === "object") {
|
||||
return !Object.entries(a).some( ([k, v]) => !deepEqual(v, b[k])) &&
|
||||
!Object.entries(b).some( ([k, v]) => !deepEqual(v, a[k]));
|
||||
} else {
|
||||
return a === b;
|
||||
}
|
||||
}
|
||||
|
||||
/** Traverses an object and sets a nested value.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* const obj = {a: {b: {c: "X"} } }
|
||||
* deepSet(obj, ["a", "b", "c"], "d")
|
||||
* → {a: {b: {c: "d"} } }
|
||||
*
|
||||
* This would be the equivalent of:
|
||||
*
|
||||
* obj?.a?.b?.c = "d";
|
||||
*
|
||||
* Except that the above is not a legal expression.
|
||||
*
|
||||
* If a non-leaf property does not exist, this function
|
||||
* creates it as an empty object ({}) and keeps traversing.
|
||||
*
|
||||
* The last member of `path` may be `null`, in which case,
|
||||
* if the object pointed to by the next to last member is
|
||||
* an array, an insert operation will take place.
|
||||
*
|
||||
*/
|
||||
function deepSet (obj, path, value) {
|
||||
const key = path.shift();
|
||||
if (!path.length) {
|
||||
if (key === null && Array.isArray(obj)) {
|
||||
obj.push(value);
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
} else {
|
||||
if (!Object.hasOwn(obj, key)) {
|
||||
obj[key] = {};
|
||||
}
|
||||
deepSet(obj[key], path, value);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a nested property.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* const obj = {a: {b: {c: "d"} } }
|
||||
* deepSet(obj, ["a", "b", "c"])
|
||||
* → "d"
|
||||
*
|
||||
* If `path` is known in advance, this is effectively
|
||||
* the same as:
|
||||
*
|
||||
* obj?.a?.b?.c
|
||||
*
|
||||
* This might be useful when `path` is dynamic.
|
||||
*/
|
||||
function deepValue (obj, path) {
|
||||
if (obj !== undefined) {
|
||||
const key = path.shift();
|
||||
if (!path.length) {
|
||||
if (key === undefined) {
|
||||
return obj;
|
||||
} else {
|
||||
return obj[key];
|
||||
}
|
||||
} else {
|
||||
return deepValue(obj[key], path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Just to have all the deep*()s in one place
|
||||
import deepMerge from './deepMerge'
|
||||
|
||||
export {
|
||||
withParentProps,
|
||||
geometryAsString,
|
||||
preferencesλ
|
||||
preferencesλ,
|
||||
deepMerge,
|
||||
deepCompare,
|
||||
deepEqual,
|
||||
deepSet,
|
||||
deepValue
|
||||
}
|
||||
|
||||
14
lib/www/client/source/src/lib/watcher-mixin.js
Normal file
14
lib/www/client/source/src/lib/watcher-mixin.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { deepCompare } from './utils';
|
||||
|
||||
function setIfDifferent(propsLocals) {
|
||||
return Object.fromEntries(Object.entries(propsLocals).map( ([prop, local]) => [
|
||||
local,
|
||||
() => {
|
||||
if (!deepCompare(this[prop], this[local])){
|
||||
this[local] = structuredClone(this[prop]);
|
||||
}
|
||||
}
|
||||
]));
|
||||
}
|
||||
|
||||
export default setIfDifferent;
|
||||
@@ -7,6 +7,8 @@ import vueDebounce from 'vue-debounce'
|
||||
import { mapMutations } from 'vuex';
|
||||
import { markdown, markdownInline } from './lib/markdown';
|
||||
import { geometryAsString } from './lib/utils';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
@@ -14,7 +16,7 @@ Vue.use(vueDebounce);
|
||||
|
||||
Vue.filter('markdown', markdown);
|
||||
Vue.filter('markdownInline', markdownInline);
|
||||
Vue.filter('position', (str, item, opts) =>
|
||||
Vue.filter('position', (str, item, opts) =>
|
||||
str
|
||||
.replace(/@POS(ITION)?@/g, geometryAsString(item, opts) || "(position unknown)")
|
||||
.replace(/@DMS@/g, geometryAsString(item, {...opts, dms:true}) || "(position unknown)")
|
||||
@@ -32,16 +34,24 @@ new Vue({
|
||||
user: null,
|
||||
|
||||
wsUrl: "/ws",
|
||||
ws: null
|
||||
ws: null,
|
||||
wsCredentialsCheckInterval: 300*1000, // ms
|
||||
wsCredentialsCheckTimer: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(["jwt"])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
markdown (value) {
|
||||
return typeof value == "string"
|
||||
? marked(value)
|
||||
: value;
|
||||
return markdown(value);
|
||||
},
|
||||
|
||||
markdownInline (value) {
|
||||
return markdownInline(value);
|
||||
},
|
||||
|
||||
showSnack(text, colour = "primary") {
|
||||
@@ -85,6 +95,17 @@ new Vue({
|
||||
this.setServerConnectionState(false);
|
||||
});
|
||||
|
||||
if (this.wsCredentialsCheckTimer) {
|
||||
clearInterval(this.wsCredentialsCheckTimer);
|
||||
this.wsCredentialsCheckTimer = null;
|
||||
}
|
||||
|
||||
this.wsCredentialsCheckTimer = setInterval( () => {
|
||||
this.ws.send(JSON.stringify({
|
||||
jwt: this.jwt
|
||||
}));
|
||||
}, this.wsCredentialsCheckInterval);
|
||||
|
||||
},
|
||||
|
||||
...mapMutations(['setServerEvent', 'setServerConnectionState'])
|
||||
|
||||
35
lib/www/client/source/src/mixins/access.js
Normal file
35
lib/www/client/source/src/mixins/access.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Organisations } from '@dougal/organisations';
|
||||
|
||||
export default {
|
||||
name: "AccessMixin",
|
||||
|
||||
computed: {
|
||||
...mapGetters(['user', 'projectConfiguration'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
access (operation, organisations) {
|
||||
if (this.user) {
|
||||
if (!organisations) organisations = this.projectConfiguration?.organisations;
|
||||
if (!organisations instanceof Organisations) {
|
||||
organisations = new Organisations(organisations);
|
||||
}
|
||||
return this.user.canDo(operation, organisations);
|
||||
}
|
||||
},
|
||||
|
||||
readaccess (item) {
|
||||
return this.access('read', item);
|
||||
},
|
||||
|
||||
writeaccess (item) {
|
||||
return this.access('write', item);
|
||||
},
|
||||
|
||||
adminaccess (item) {
|
||||
return this.access('edit', item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,10 @@ import Log from '../views/Log.vue'
|
||||
import QC from '../views/QC.vue'
|
||||
import Graphs from '../views/Graphs.vue'
|
||||
import Map from '../views/Map.vue'
|
||||
import ProjectSettings from '../views/ProjectSettings.vue'
|
||||
import Users from '../views/Users.vue'
|
||||
import DougalAppBarExtensionProject from '../components/app-bar-extension-project'
|
||||
import DougalAppBarExtensionProjectList from '../components/app-bar-extension-project-list'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
@@ -47,6 +50,19 @@ Vue.use(VueRouter)
|
||||
name: "equipment",
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/Equipment.vue')
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
path: "/users",
|
||||
redirect: "/users/"
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
name: "Users",
|
||||
path: "/users/",
|
||||
component: Users,
|
||||
meta: {
|
||||
}
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
path: "/login",
|
||||
@@ -80,7 +96,10 @@ Vue.use(VueRouter)
|
||||
meta: {
|
||||
breadcrumbs: [
|
||||
{ text: "Projects", href: "/projects", disabled: true }
|
||||
]
|
||||
],
|
||||
appBarExtension: {
|
||||
// component: DougalAppBarExtensionProjectList
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -98,7 +117,9 @@ Vue.use(VueRouter)
|
||||
{ text: "Projects", href: "/projects" },
|
||||
{
|
||||
text: (ctx) => ctx.$store.state.project.projectName || "…",
|
||||
href: (ctx) => `/projects/${ctx.$store.state.project.projectId || ctx.$route.params.project || ""}/`
|
||||
href: (ctx) => `/projects/${ctx.$store.state.project.projectId || ctx.$route.params.project || ""}/`,
|
||||
title: (ctx) => Object.entries(ctx.$store.getters.projectConfiguration?.organisations ?? {}).map( ([org, ops]) => `* ${org}: ${Object.entries(ops).filter( ([k, v]) => v ).map( ([k, v]) => k ).join(", ")}`).join("\n"),
|
||||
organisations: (ctx) => ctx.$store.getters.projectConfiguration?.organisations ?? {}
|
||||
}
|
||||
],
|
||||
appBarExtension: {
|
||||
@@ -168,6 +189,11 @@ Vue.use(VueRouter)
|
||||
path: "map",
|
||||
name: "map",
|
||||
component: Map
|
||||
},
|
||||
{
|
||||
path: "configuration",
|
||||
name: "configuration",
|
||||
component: ProjectSettings
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
async function api ({state, commit, dispatch}, [resource, init = {}, cb]) {
|
||||
async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb, opts = {}]) {
|
||||
try {
|
||||
commit("queueRequest");
|
||||
if (init && init.hasOwnProperty("body")) {
|
||||
@@ -9,9 +9,16 @@ async function api ({state, commit, dispatch}, [resource, init = {}, cb]) {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
if (typeof init.body != "string") {
|
||||
init.body = JSON.stringify(init.body);
|
||||
}
|
||||
}
|
||||
if (!init.headers) {
|
||||
init.headers = {};
|
||||
}
|
||||
// We also send Authorization: Bearer …
|
||||
if (getters.jwt) {
|
||||
init.headers["Authorization"] = "Bearer "+getters.jwt;
|
||||
}
|
||||
if (typeof init.body != "string") {
|
||||
init.body = JSON.stringify(init.body);
|
||||
}
|
||||
const url = /^https?:\/\//i.test(resource) ? resource : (state.apiUrl + resource);
|
||||
const res = await fetch(url, init);
|
||||
@@ -42,7 +49,9 @@ async function api ({state, commit, dispatch}, [resource, init = {}, cb]) {
|
||||
message = body.message;
|
||||
}
|
||||
}
|
||||
await dispatch('showSnack', [message, "warning"]);
|
||||
if (!opts?.silent) {
|
||||
await dispatch('showSnack', [message, "warning"]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && err.name == "AbortError") return;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
|
||||
async function getProject ({commit, dispatch}, projectId) {
|
||||
const res = await dispatch('api', [`/project/${String(projectId).toLowerCase()}/configuration`]);
|
||||
const init = {
|
||||
headers: {
|
||||
"If-None-Match": "" // Ensure we get a fresh response
|
||||
}
|
||||
};
|
||||
const res = await dispatch('api', [`/project/${String(projectId).toLowerCase()}/configuration`, init]);
|
||||
if (res) {
|
||||
commit('setProjectName', res.name);
|
||||
commit('setProjectId', res.id?.toLowerCase());
|
||||
|
||||
@@ -13,7 +13,7 @@ async function refreshProjects ({commit, dispatch, state, rootState}) {
|
||||
const init = {
|
||||
signal: state.loading.signal
|
||||
};
|
||||
const res = await dispatch('api', [url, init]);
|
||||
const res = await dispatch('api', [url, init, null, {silent:true}]);
|
||||
|
||||
if (res) {
|
||||
commit('setProjects', res);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import jwt_decode from 'jwt-decode';
|
||||
import { User } from '@/lib/user';
|
||||
|
||||
async function login ({commit, dispatch}, loginRequest) {
|
||||
const url = "/login";
|
||||
@@ -11,7 +12,7 @@ async function login ({commit, dispatch}, loginRequest) {
|
||||
}
|
||||
const res = await dispatch('api', [url, init]);
|
||||
if (res && res.ok) {
|
||||
await dispatch('setCredentials', true);
|
||||
await dispatch('setCredentials', {force: true});
|
||||
await dispatch('loadUserPreferences');
|
||||
}
|
||||
}
|
||||
@@ -34,13 +35,13 @@ function cookieChanged (cookie) {
|
||||
return browserCookie != cookie;
|
||||
}
|
||||
|
||||
function setCredentials ({state, commit, getters, dispatch}, force = false) {
|
||||
if (cookieChanged(state.cookie) || force) {
|
||||
function setCredentials ({state, commit, getters, dispatch, rootState}, {force, token} = {}) {
|
||||
if (token || force || cookieChanged(state.cookie)) {
|
||||
try {
|
||||
const cookie = browserCookie();
|
||||
const decoded = cookie ? jwt_decode(cookie.split("=")[1]) : null;
|
||||
commit('setCookie', cookie);
|
||||
commit('setUser', decoded);
|
||||
const decoded = (token ?? cookie) ? jwt_decode(token ?? cookie.split("=")[1]) : null;
|
||||
commit('setCookie', (cookie ?? (token && ("JWT="+token))) || undefined);
|
||||
commit('setUser', decoded ? new User(decoded, rootState.api.api) : null);
|
||||
} catch (err) {
|
||||
if (err.name == "InvalidTokenError") {
|
||||
console.warn("Failed to decode", browserCookie());
|
||||
@@ -54,13 +55,13 @@ function setCredentials ({state, commit, getters, dispatch}, force = false) {
|
||||
|
||||
/**
|
||||
* Save user preferences to localStorage and store.
|
||||
*
|
||||
*
|
||||
* User preferences are identified by a key that gets
|
||||
* prefixed with the user name and role. The value can
|
||||
* prefixed with the user ID. The value can
|
||||
* be anything that JSON.stringify can parse.
|
||||
*/
|
||||
function saveUserPreference ({state, commit}, [key, value]) {
|
||||
const k = `${state.user?.name}.${state.user?.role}.${key}`;
|
||||
const k = `${state.user?.id}.${key}`;
|
||||
|
||||
if (value !== undefined) {
|
||||
localStorage.setItem(k, JSON.stringify(value));
|
||||
@@ -79,7 +80,7 @@ function saveUserPreference ({state, commit}, [key, value]) {
|
||||
|
||||
async function loadUserPreferences ({state, commit}) {
|
||||
// Get all keys which are of interest to us
|
||||
const prefix = `${state.user?.name}.${state.user?.role}`;
|
||||
const prefix = `${state.user?.id}`;
|
||||
const keys = Object.keys(localStorage).filter( k => k.startsWith(prefix) );
|
||||
|
||||
// Build the preferences object
|
||||
|
||||
@@ -3,16 +3,14 @@ function user (state) {
|
||||
return state.user;
|
||||
}
|
||||
|
||||
function writeaccess (state) {
|
||||
return state.user && ["user", "admin"].includes(state.user.role);
|
||||
}
|
||||
|
||||
function adminaccess (state) {
|
||||
return state.user && state.user.role == "admin";
|
||||
function jwt (state) {
|
||||
if (state.cookie?.startsWith("JWT=")) {
|
||||
return state.cookie.substring(4);
|
||||
}
|
||||
}
|
||||
|
||||
function preferences (state) {
|
||||
return state.preferences;
|
||||
}
|
||||
|
||||
export default { user, writeaccess, adminaccess, preferences };
|
||||
export default { user, jwt, preferences };
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@input="closeDialog"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-if="writeaccess"
|
||||
<v-btn v-if="writeaccess()"
|
||||
small
|
||||
color="primary"
|
||||
v-bind="attrs"
|
||||
@@ -182,7 +182,7 @@
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn v-if="writeaccess"
|
||||
<v-btn v-if="writeaccess()"
|
||||
small
|
||||
text
|
||||
color="primary"
|
||||
@@ -205,7 +205,7 @@
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="writeaccess"
|
||||
<v-btn v-if="writeaccess()"
|
||||
small
|
||||
dark
|
||||
color="red"
|
||||
@@ -247,7 +247,7 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="writeaccess"
|
||||
<v-btn v-if="writeaccess()"
|
||||
small
|
||||
dark
|
||||
color="red"
|
||||
@@ -303,10 +303,15 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: "Equipment",
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
latest: [],
|
||||
@@ -395,7 +400,7 @@ export default {
|
||||
return null;
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ export default {
|
||||
return this.sequences[0]?.sequence;
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'preferences', 'writeaccess', 'loading', 'serverEvent'])
|
||||
...mapGetters(['user', 'preferences', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
|
||||
<v-menu v-if="writeaccess"
|
||||
<v-menu v-if="writeaccess()"
|
||||
v-model="contextMenuShow"
|
||||
:position-x="contextMenuX"
|
||||
:position-y="contextMenuY"
|
||||
@@ -164,7 +164,7 @@
|
||||
</v-text-field>
|
||||
<div v-else>
|
||||
<span v-html="$options.filters.markdownInline(item.remarks)"></span>
|
||||
<v-btn v-if="writeaccess && edit === null"
|
||||
<v-btn v-if="writeaccess() && edit === null"
|
||||
icon
|
||||
small
|
||||
title="Edit"
|
||||
@@ -196,6 +196,7 @@
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import DougalLineStatus from '@/components/line-status';
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: "LineList",
|
||||
@@ -204,6 +205,10 @@ export default {
|
||||
DougalLineStatus
|
||||
},
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
headers: [
|
||||
@@ -281,7 +286,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['user', 'writeaccess', 'linesLoading', 'lines', 'sequences', 'plannedSequences'])
|
||||
...mapGetters(['user', 'linesLoading', 'lines', 'sequences', 'plannedSequences'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
>mdi-format-list-numbered</v-icon>
|
||||
</a>
|
||||
|
||||
<dougal-event-edit v-if="writeaccess"
|
||||
<dougal-event-edit v-if="$parent.writeaccess()"
|
||||
v-model="eventDialog"
|
||||
v-bind="editedEvent"
|
||||
:available-labels="userLabels"
|
||||
@@ -54,7 +54,7 @@
|
||||
>
|
||||
</dougal-event-edit>
|
||||
|
||||
<dougal-event-edit-labels v-if="writeaccess"
|
||||
<dougal-event-edit-labels v-if="$parent.writeaccess()"
|
||||
v-model="eventLabelsDialog"
|
||||
:labels="userLabels"
|
||||
:selected="contextMenuItem ? contextMenuItem.labels||[] : []"
|
||||
@@ -171,7 +171,7 @@
|
||||
<v-card-text>
|
||||
|
||||
<!-- BEGIN Context menu for log entries -->
|
||||
<v-menu v-if="writeaccess"
|
||||
<v-menu v-if="$parent.writeaccess()"
|
||||
v-model="contextMenuShow"
|
||||
:position-x="contextMenuX"
|
||||
:position-y="contextMenuY"
|
||||
@@ -325,7 +325,7 @@
|
||||
@click="labelSearch=label"
|
||||
>{{label}}</v-chip>
|
||||
</span>
|
||||
<dougal-event-edit-history v-if="entry.has_edits && writeaccess"
|
||||
<dougal-event-edit-history v-if="entry.has_edits && $parent.writeaccess()"
|
||||
:id="entry.id"
|
||||
:disabled="eventsLoading"
|
||||
:labels="labels"
|
||||
@@ -488,6 +488,9 @@ export default {
|
||||
rows () {
|
||||
const rows = {};
|
||||
this.items
|
||||
.filter(i => {
|
||||
return !this.$route.params.sequence || (this.$route.params.sequence == i.sequence)
|
||||
})
|
||||
.filter(i => {
|
||||
for (const label of this.filterableLabels) {
|
||||
if (!this.shownLabels.includes(label) && i.labels.includes(label)) {
|
||||
@@ -538,7 +541,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'writeaccess', 'eventsLoading', 'online', 'sequence', 'line', 'point', 'position', 'timestamp', 'lineName', 'events', 'labels', 'userLabels', 'projectConfiguration']),
|
||||
...mapGetters(['user', 'eventsLoading', 'online', 'sequence', 'line', 'point', 'position', 'timestamp', 'lineName', 'events', 'labels', 'userLabels', 'projectConfiguration']),
|
||||
...mapState({projectSchema: state => state.project.projectSchema})
|
||||
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user