mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 09:57:09 +00:00
Compare commits
194 Commits
v2025.30.1
...
v2025.33.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12a762f44f | ||
|
|
ebf13abc28 | ||
|
|
b3552db02f | ||
|
|
cd882c0611 | ||
|
|
6fc9c020a4 | ||
|
|
75284322f1 | ||
|
|
e849c47f01 | ||
|
|
387d20a4f0 | ||
|
|
2fab06d340 | ||
|
|
7d2fb5558a | ||
|
|
764e2cfb23 | ||
|
|
bf1af1f76c | ||
|
|
09e4cd2467 | ||
|
|
2009d73a2b | ||
|
|
083ee812de | ||
|
|
84510e8dc9 | ||
|
|
7205ec42a8 | ||
|
|
73d85ef81f | ||
|
|
6c4dc35461 | ||
|
|
a5ebff077d | ||
|
|
2a894692ce | ||
|
|
25690eeb52 | ||
|
|
3f9776b61d | ||
|
|
8c81daefc0 | ||
|
|
c173610e87 | ||
|
|
301e5c0731 | ||
|
|
48d9f45fe0 | ||
|
|
cd23a78592 | ||
|
|
e368183bf0 | ||
|
|
02477b071b | ||
|
|
6651868ea7 | ||
|
|
c0b52a8245 | ||
|
|
90ce6f063e | ||
|
|
b2fa0c3d40 | ||
|
|
83ecaad4fa | ||
|
|
1c5fd2e34d | ||
|
|
aabcc74891 | ||
|
|
2a7b51b995 | ||
|
|
5d19ca7ca7 | ||
|
|
910195fc0f | ||
|
|
6e5570aa7c | ||
|
|
595c20f504 | ||
|
|
40d0038d80 | ||
|
|
acdf118a67 | ||
|
|
b9e0975d3d | ||
|
|
39d9c9d748 | ||
|
|
b8b25dcd62 | ||
|
|
db97382758 | ||
|
|
ae8e5d4ef6 | ||
|
|
2c1a24e4a5 | ||
|
|
0b83187372 | ||
|
|
3dd51c82ea | ||
|
|
17e6564e70 | ||
|
|
3a769e7fd0 | ||
|
|
7dde0a15c6 | ||
|
|
2872af8d60 | ||
|
|
4e581d5664 | ||
|
|
a188e9a099 | ||
|
|
cd6ad92d5c | ||
|
|
08dfe7ef0a | ||
|
|
6a5238496e | ||
|
|
bc237cb685 | ||
|
|
4957142fb1 | ||
|
|
5a19c81ed1 | ||
|
|
b583dc6c02 | ||
|
|
134e3bce4e | ||
|
|
f5ad9d7182 | ||
|
|
07874ffe0b | ||
|
|
695add5da6 | ||
|
|
6a94287cba | ||
|
|
c2ec2970f0 | ||
|
|
95d6d0054b | ||
|
|
5070be5ff3 | ||
|
|
d5e77bc946 | ||
|
|
f6faad17db | ||
|
|
94cdf83b13 | ||
|
|
6a788ae28b | ||
|
|
544117eec3 | ||
|
|
e5679ec14b | ||
|
|
a1c174994c | ||
|
|
2db8cc3116 | ||
|
|
99b1a841c5 | ||
|
|
6629e25644 | ||
|
|
7f5f64acb1 | ||
|
|
8f87df1e2f | ||
|
|
8399782409 | ||
|
|
9c86018653 | ||
|
|
a15c97078b | ||
|
|
d769ec48dd | ||
|
|
fe421f545c | ||
|
|
caa8fec8cc | ||
|
|
49fc260ace | ||
|
|
b7038f542c | ||
|
|
40ad0e7650 | ||
|
|
9006deb8be | ||
|
|
6e19b8e18f | ||
|
|
3d474ad8f8 | ||
|
|
821af18f29 | ||
|
|
9cf15ce9dd | ||
|
|
78838cbc41 | ||
|
|
8855da743b | ||
|
|
c67a60a7e6 | ||
|
|
81e06930f0 | ||
|
|
0263eab6d1 | ||
|
|
931219850e | ||
|
|
12369d5419 | ||
|
|
447003c3b5 | ||
|
|
be7157b62c | ||
|
|
8ef56f9946 | ||
|
|
f2df16fe55 | ||
|
|
96db6b1376 | ||
|
|
36d86c176a | ||
|
|
9c38af4bc0 | ||
|
|
be5c6f1fa3 | ||
|
|
17b9d60715 | ||
|
|
e2dd563054 | ||
|
|
67dcc2922b | ||
|
|
11e84f47eb | ||
|
|
1066a03b25 | ||
|
|
08440e3e21 | ||
|
|
d46eb3b455 | ||
|
|
864b430320 | ||
|
|
61cbefd0e9 | ||
|
|
29c484affa | ||
|
|
0806b80445 | ||
|
|
b5a3a22892 | ||
|
|
c13aa23e2f | ||
|
|
3366377ab0 | ||
|
|
59a90e352c | ||
|
|
0f207f8c2d | ||
|
|
c97eaa64f5 | ||
|
|
5b82f8540d | ||
|
|
d977d9c40b | ||
|
|
d16fb41f24 | ||
|
|
c376896ea6 | ||
|
|
2bcdee03d5 | ||
|
|
44113c89c0 | ||
|
|
17c6d9d1e5 | ||
|
|
06cc16721f | ||
|
|
af7485370c | ||
|
|
ad013ea642 | ||
|
|
48d5986415 | ||
|
|
471f4e8e64 | ||
|
|
4be99370e6 | ||
|
|
e464f5f887 | ||
|
|
cc8d790ad8 | ||
|
|
32c6e2c79f | ||
|
|
ba7221ae10 | ||
|
|
1cb9d4b1e2 | ||
|
|
2a0025cdbf | ||
|
|
f768f31b62 | ||
|
|
9f91b1317f | ||
|
|
3b69a15703 | ||
|
|
cd3bd8ab79 | ||
|
|
df193a99cd | ||
|
|
580e94a591 | ||
|
|
3413641c10 | ||
|
|
f092aff015 | ||
|
|
94c6406ea2 | ||
|
|
244d84a3bd | ||
|
|
89c565a0f5 | ||
|
|
31ac8d3c01 | ||
|
|
3bb78040b0 | ||
|
|
1433bda14e | ||
|
|
c0ae033de8 | ||
|
|
05eed7ef26 | ||
|
|
5d2ca513a6 | ||
|
|
b9c8069828 | ||
|
|
b80b8ffb52 | ||
|
|
c2eb82ffe7 | ||
|
|
e517e2f771 | ||
|
|
0afd54447f | ||
|
|
e6004dd62f | ||
|
|
f623954399 | ||
|
|
f8d882da5d | ||
|
|
808c9987af | ||
|
|
4db6d8dd7a | ||
|
|
9a47977f5f | ||
|
|
a58cce8565 | ||
|
|
5487a3a49b | ||
|
|
731778206c | ||
|
|
08e65b512d | ||
|
|
9b05388113 | ||
|
|
1b44389a1a | ||
|
|
0b3711b759 | ||
|
|
5a523d4941 | ||
|
|
122951e3a2 | ||
|
|
90216c12e4 | ||
|
|
9c26909a59 | ||
|
|
0427a3c18c | ||
|
|
c32e6f2b38 | ||
|
|
546d199c52 | ||
|
|
6562de97b9 | ||
|
|
c666a6368e |
@@ -23,6 +23,7 @@ transform = {
|
||||
}
|
||||
|
||||
def parse_line (line, fields, fixed = None):
|
||||
# print("parse_line", line, fields, fixed)
|
||||
data = dict()
|
||||
|
||||
if fixed:
|
||||
@@ -51,6 +52,7 @@ def parse_line (line, fields, fixed = None):
|
||||
|
||||
data[key] = value
|
||||
|
||||
# print("parse_line data =", data)
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,12 @@ if __name__ == '__main__':
|
||||
|
||||
lineNameInfo = final_p111.get("lineNameInfo")
|
||||
pattern = final_p111.get("pattern")
|
||||
if not lineNameInfo:
|
||||
if not pattern:
|
||||
print("ERROR! Missing final.p111.lineNameInfo in project configuration. Cannot import final P111")
|
||||
raise Exception("Missing final.p111.lineNameInfo")
|
||||
else:
|
||||
print("WARNING! No `lineNameInfo` in project configuration (final.p111). You should add it to the settings.")
|
||||
rx = None
|
||||
if pattern and pattern.get("regex"):
|
||||
rx = re.compile(pattern["regex"])
|
||||
@@ -114,27 +120,27 @@ if __name__ == '__main__':
|
||||
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
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -41,6 +41,12 @@ if __name__ == '__main__':
|
||||
|
||||
lineNameInfo = raw_p111.get("lineNameInfo")
|
||||
pattern = raw_p111.get("pattern")
|
||||
if not lineNameInfo:
|
||||
if not pattern:
|
||||
print("ERROR! Missing raw.p111.lineNameInfo in project configuration. Cannot import raw P111")
|
||||
raise Exception("Missing raw.p111.lineNameInfo")
|
||||
else:
|
||||
print("WARNING! No `lineNameInfo` in project configuration (raw.p111). You should add it to the settings.")
|
||||
rx = None
|
||||
if pattern and pattern.get("regex"):
|
||||
rx = re.compile(pattern["regex"])
|
||||
@@ -96,14 +102,15 @@ if __name__ == '__main__':
|
||||
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
|
||||
}
|
||||
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")
|
||||
|
||||
@@ -38,6 +38,7 @@ CREATE OR REPLACE PROCEDURE pg_temp.upgrade_database () AS $outer$
|
||||
BEGIN
|
||||
|
||||
RAISE NOTICE 'Updating schema %', 'public';
|
||||
SET search_path TO public;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS keystore (
|
||||
type TEXT NOT NULL, -- A class of data to be stored
|
||||
@@ -79,7 +80,7 @@ BEGIN
|
||||
USING MESSAGE='Patch already applied';
|
||||
END IF;
|
||||
|
||||
IF current_db_version != '0.5.3' THEN
|
||||
IF current_db_version != '0.5.4' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Invalid database version: ' || current_db_version,
|
||||
HINT='Ensure all previous patches have been applied.';
|
||||
@@ -41,6 +41,7 @@ CREATE OR REPLACE PROCEDURE pg_temp.upgrade_database () AS $outer$
|
||||
BEGIN
|
||||
|
||||
RAISE NOTICE 'Updating schema %', 'public';
|
||||
SET search_path TO public;
|
||||
|
||||
INSERT INTO keystore (type, key, data)
|
||||
VALUES ('user', '6f1e7159-4ca0-4ae4-ab4e-89078166cc10', '
|
||||
@@ -0,0 +1,106 @@
|
||||
-- Fix final_lines_summary view
|
||||
--
|
||||
-- New schema version: 0.6.2
|
||||
--
|
||||
-- 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 an "organisations" section to the configuration,
|
||||
-- with a default configured organisation of "WGP" with full access.
|
||||
-- This is so that projects can be made accessible after migrating
|
||||
-- to the new permissions architecture.
|
||||
--
|
||||
-- In addition, projects with an id starting with "eq" are assumed to
|
||||
-- be Equinor projects, and an additional organisation is added with
|
||||
-- read-only access. This is intended for clients, which should be
|
||||
-- assigned to the "Equinor organisation".
|
||||
--
|
||||
-- Finally, we assign the vessel to the "WGP" organisation (full access)
|
||||
-- so that we can actually use administrative endpoints.
|
||||
--
|
||||
-- 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';
|
||||
SET search_path TO public;
|
||||
|
||||
-- Add "organisations" section to configurations, if not already present
|
||||
UPDATE projects
|
||||
SET
|
||||
meta = jsonb_set(meta, '{organisations}', '{"WGP": {"read": true, "write": true, "edit": true}}'::jsonb, true)
|
||||
WHERE meta->'organisations' IS NULL;
|
||||
|
||||
-- Add (or overwrite!) "organisations.Equinor" giving read-only access (can be changed later via API)
|
||||
UPDATE projects
|
||||
SET
|
||||
meta = jsonb_set(meta, '{organisations, Equinor}', '{"read": true, "write": false, "edit": false}'::jsonb, true)
|
||||
WHERE pid LIKE 'eq%';
|
||||
|
||||
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.2' THEN
|
||||
RAISE EXCEPTION
|
||||
USING MESSAGE='Patch already applied';
|
||||
END IF;
|
||||
|
||||
IF current_db_version != '0.6.1' 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.2"}')
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = public.info.value || '{"db_schema": "0.6.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
|
||||
--
|
||||
907
lib/modules/@dougal/binary/classes.js
Normal file
907
lib/modules/@dougal/binary/classes.js
Normal file
@@ -0,0 +1,907 @@
|
||||
const codeToType = {
|
||||
0: Int8Array,
|
||||
1: Uint8Array,
|
||||
2: Int16Array,
|
||||
3: Uint16Array,
|
||||
4: Int32Array,
|
||||
5: Uint32Array,
|
||||
7: Float32Array,
|
||||
8: Float64Array,
|
||||
9: BigInt64Array,
|
||||
10: BigUint64Array
|
||||
};
|
||||
|
||||
const typeToBytes = {
|
||||
Int8Array: 1,
|
||||
Uint8Array: 1,
|
||||
Int16Array: 2,
|
||||
Uint16Array: 2,
|
||||
Int32Array: 4,
|
||||
Uint32Array: 4,
|
||||
Float32Array: 4,
|
||||
Float64Array: 8,
|
||||
BigInt64Array: 8,
|
||||
BigUint64Array: 8
|
||||
};
|
||||
|
||||
function readTypedValue(view, offset, type) {
|
||||
switch (type) {
|
||||
case Int8Array: return view.getInt8(offset);
|
||||
case Uint8Array: return view.getUint8(offset);
|
||||
case Int16Array: return view.getInt16(offset, true);
|
||||
case Uint16Array: return view.getUint16(offset, true);
|
||||
case Int32Array: return view.getInt32(offset, true);
|
||||
case Uint32Array: return view.getUint32(offset, true);
|
||||
case Float32Array: return view.getFloat32(offset, true);
|
||||
case Float64Array: return view.getFloat64(offset, true);
|
||||
case BigInt64Array: return view.getBigInt64(offset, true);
|
||||
case BigUint64Array: return view.getBigUint64(offset, true);
|
||||
default: throw new Error(`Unsupported type: ${type.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
function writeTypedValue(view, offset, value, type) {
|
||||
switch (type) {
|
||||
case Int8Array: view.setInt8(offset, value); break;
|
||||
case Uint8Array: view.setUint8(offset, value); break;
|
||||
case Int16Array: view.setInt16(offset, value, true); break;
|
||||
case Uint16Array: view.setUint16(offset, value, true); break;
|
||||
case Int32Array: view.setInt32(offset, value, true); break;
|
||||
case Uint32Array: view.setUint32(offset, value, true); break;
|
||||
case Float32Array: view.setFloat32(offset, value, true); break;
|
||||
case Float64Array: view.setFloat64(offset, value, true); break;
|
||||
case BigInt64Array: view.setBigInt64(offset, BigInt(value), true); break;
|
||||
case BigUint64Array: view.setBigUint64(offset, BigInt(value), true); break;
|
||||
default: throw new Error(`Unsupported type: ${type.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
class DougalBinaryBundle extends ArrayBuffer {
|
||||
|
||||
static HEADER_LENGTH = 4; // Length of a bundle header
|
||||
|
||||
/** Clone an existing ByteArray into a DougalBinaryBundle
|
||||
*/
|
||||
static clone (buffer) {
|
||||
const clone = new DougalBinaryBundle(buffer.byteLength);
|
||||
const uint8Array = new Uint8Array(buffer);
|
||||
const uint8ArrayClone = new Uint8Array(clone);
|
||||
uint8ArrayClone.set(uint8Array);
|
||||
return clone;
|
||||
}
|
||||
|
||||
constructor (length, options) {
|
||||
super (length, options);
|
||||
}
|
||||
|
||||
/** Get the count of bundles in this ByteArray.
|
||||
*
|
||||
* Stops at the first non-bundle looking offset
|
||||
*/
|
||||
get bundleCount () {
|
||||
let count = 0;
|
||||
let currentBundleOffset = 0;
|
||||
const view = new DataView(this);
|
||||
|
||||
while (currentBundleOffset < this.byteLength) {
|
||||
|
||||
const currentBundleHeader = view.getUint32(currentBundleOffset, true);
|
||||
if ((currentBundleHeader & 0xff) !== 0x1c) {
|
||||
// This is not a bundle
|
||||
return count;
|
||||
}
|
||||
let currentBundleLength = currentBundleHeader >>> 8;
|
||||
|
||||
currentBundleOffset += currentBundleLength + DougalBinaryBundle.HEADER_LENGTH;
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
/** Get the number of chunks in the bundles of this ByteArray
|
||||
*/
|
||||
get chunkCount () {
|
||||
let count = 0;
|
||||
let bundleOffset = 0;
|
||||
const view = new DataView(this);
|
||||
|
||||
while (bundleOffset < this.byteLength) {
|
||||
const header = view.getUint32(bundleOffset, true);
|
||||
if ((header & 0xFF) !== 0x1C) break;
|
||||
const length = header >>> 8;
|
||||
if (bundleOffset + 4 + length > this.byteLength) break;
|
||||
|
||||
let chunkOffset = bundleOffset + 4; // relative to buffer start
|
||||
|
||||
while (chunkOffset < bundleOffset + 4 + length) {
|
||||
const chunkType = view.getUint8(chunkOffset);
|
||||
if (chunkType !== 0x11 && chunkType !== 0x12) break;
|
||||
|
||||
const cCount = view.getUint16(chunkOffset + 2, true);
|
||||
const ΔelemC = view.getUint8(chunkOffset + 10);
|
||||
const elemC = view.getUint8(chunkOffset + 11);
|
||||
|
||||
let localOffset = 12; // header size
|
||||
|
||||
localOffset += ΔelemC + elemC; // preface
|
||||
|
||||
// initial values
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const typeByte = view.getUint8(chunkOffset + 12 + k);
|
||||
const baseCode = typeByte & 0xF;
|
||||
const baseType = codeToType[baseCode];
|
||||
if (!baseType) throw new Error('Invalid base type code');
|
||||
localOffset += typeToBytes[baseType.name];
|
||||
}
|
||||
|
||||
// pad after initial
|
||||
while (localOffset % 4 !== 0) localOffset++;
|
||||
|
||||
if (chunkType === 0x11) { // Sequential
|
||||
// record data: Δelems incrs
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const typeByte = view.getUint8(chunkOffset + 12 + k);
|
||||
const incrCode = typeByte >> 4;
|
||||
const incrType = codeToType[incrCode];
|
||||
if (!incrType) throw new Error('Invalid incr type code');
|
||||
localOffset += cCount * typeToBytes[incrType.name];
|
||||
}
|
||||
|
||||
// elems
|
||||
for (let k = 0; k < elemC; k++) {
|
||||
const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k);
|
||||
const type = codeToType[typeCode];
|
||||
if (!type) throw new Error('Invalid elem type code');
|
||||
localOffset += cCount * typeToBytes[type.name];
|
||||
}
|
||||
} else { // Interleaved
|
||||
// Compute exact stride for interleaved record data
|
||||
let ΔelemStride = 0;
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const typeByte = view.getUint8(chunkOffset + 12 + k);
|
||||
const incrCode = typeByte >> 4;
|
||||
const incrType = codeToType[incrCode];
|
||||
if (!incrType) throw new Error('Invalid incr type code');
|
||||
ΔelemStride += typeToBytes[incrType.name];
|
||||
}
|
||||
let elemStride = 0;
|
||||
for (let k = 0; k < elemC; k++) {
|
||||
const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k);
|
||||
const type = codeToType[typeCode];
|
||||
if (!type) throw new Error('Invalid elem type code');
|
||||
elemStride += typeToBytes[type.name];
|
||||
}
|
||||
const recordStride = ΔelemStride + elemStride;
|
||||
localOffset += cCount * recordStride;
|
||||
}
|
||||
|
||||
// pad after record
|
||||
while (localOffset % 4 !== 0) localOffset++;
|
||||
|
||||
chunkOffset += localOffset;
|
||||
count++;
|
||||
}
|
||||
|
||||
bundleOffset += 4 + length;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Return an array of DougalBinaryChunkSequential or DougalBinaryChunkInterleaved instances
|
||||
*/
|
||||
chunks () {
|
||||
const chunks = [];
|
||||
let bundleOffset = 0;
|
||||
const view = new DataView(this);
|
||||
|
||||
while (bundleOffset < this.byteLength) {
|
||||
const header = view.getUint32(bundleOffset, true);
|
||||
if ((header & 0xFF) !== 0x1C) break;
|
||||
const length = header >>> 8;
|
||||
if (bundleOffset + 4 + length > this.byteLength) break;
|
||||
|
||||
let chunkOffset = bundleOffset + 4;
|
||||
|
||||
while (chunkOffset < bundleOffset + 4 + length) {
|
||||
const chunkType = view.getUint8(chunkOffset);
|
||||
if (chunkType !== 0x11 && chunkType !== 0x12) break;
|
||||
|
||||
const cCount = view.getUint16(chunkOffset + 2, true);
|
||||
const ΔelemC = view.getUint8(chunkOffset + 10);
|
||||
const elemC = view.getUint8(chunkOffset + 11);
|
||||
|
||||
let localOffset = 12;
|
||||
|
||||
localOffset += ΔelemC + elemC;
|
||||
|
||||
// initial values
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const typeByte = view.getUint8(chunkOffset + 12 + k);
|
||||
const baseCode = typeByte & 0xF;
|
||||
const baseType = codeToType[baseCode];
|
||||
if (!baseType) throw new Error('Invalid base type code');
|
||||
localOffset += typeToBytes[baseType.name];
|
||||
}
|
||||
|
||||
// pad after initial
|
||||
while (localOffset % 4 !== 0) localOffset++;
|
||||
|
||||
if (chunkType === 0x11) { // Sequential
|
||||
// record data: Δelems incrs
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const typeByte = view.getUint8(chunkOffset + 12 + k);
|
||||
const incrCode = typeByte >> 4;
|
||||
const incrType = codeToType[incrCode];
|
||||
if (!incrType) throw new Error('Invalid incr type code');
|
||||
localOffset += cCount * typeToBytes[incrType.name];
|
||||
}
|
||||
|
||||
// elems
|
||||
for (let k = 0; k < elemC; k++) {
|
||||
const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k);
|
||||
const type = codeToType[typeCode];
|
||||
if (!type) throw new Error('Invalid elem type code');
|
||||
localOffset += cCount * typeToBytes[type.name];
|
||||
}
|
||||
} else { // Interleaved
|
||||
// Compute exact stride for interleaved record data
|
||||
let ΔelemStride = 0;
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const typeByte = view.getUint8(chunkOffset + 12 + k);
|
||||
const incrCode = typeByte >> 4;
|
||||
const incrType = codeToType[incrCode];
|
||||
if (!incrType) throw new Error('Invalid incr type code');
|
||||
ΔelemStride += typeToBytes[incrType.name];
|
||||
}
|
||||
let elemStride = 0;
|
||||
for (let k = 0; k < elemC; k++) {
|
||||
const typeCode = view.getUint8(chunkOffset + 12 + ΔelemC + k);
|
||||
const type = codeToType[typeCode];
|
||||
if (!type) throw new Error('Invalid elem type code');
|
||||
elemStride += typeToBytes[type.name];
|
||||
}
|
||||
const recordStride = ΔelemStride + elemStride;
|
||||
localOffset += cCount * recordStride;
|
||||
}
|
||||
|
||||
// pad after record
|
||||
while (localOffset % 4 !== 0) localOffset++;
|
||||
|
||||
switch (chunkType) {
|
||||
case 0x11:
|
||||
chunks.push(new DougalBinaryChunkSequential(this, chunkOffset, localOffset));
|
||||
break;
|
||||
case 0x12:
|
||||
chunks.push(new DougalBinaryChunkInterleaved(this, chunkOffset, localOffset));
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid chunk type');
|
||||
}
|
||||
|
||||
chunkOffset += localOffset;
|
||||
}
|
||||
|
||||
bundleOffset += 4 + length;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/** Return a ByteArray containing all data from all
|
||||
* chunks including reconstructed i, j and incremental
|
||||
* values as follows:
|
||||
*
|
||||
* <i_0> <i_1> … <i_x> // i values (constant)
|
||||
* <j_0> <j_1> … <j_x> // j values (j0 + Δj*i)
|
||||
* <Δelem_0_0> <Δelem_0_1> … <Δelem_0_x> // reconstructed Δelem0 (uses baseType)
|
||||
* <Δelem_1_0> <Δelem_1_1> … <Δelem_1_x> // reconstructed Δelem1
|
||||
* …
|
||||
* <Δelem_y_0> <Δelem_y_1> … <Δelem_y_x> // reconstructed Δelem1
|
||||
* <elem_0_0> <elem_0_1> … <elem_0_x> // First elem
|
||||
* <elem_1_0> <elem_1_1> … <elem_1_x> // Second elem
|
||||
* …
|
||||
* <elem_z_0> <elem_z_1> … <elem_z_x> // Last elem
|
||||
*
|
||||
* It does not matter whether the underlying chunks are
|
||||
* sequential or interleaved. This function will transform
|
||||
* as necessary.
|
||||
*
|
||||
*/
|
||||
getDataSequentially () {
|
||||
const chunks = this.chunks();
|
||||
if (chunks.length === 0) return new ArrayBuffer(0);
|
||||
|
||||
const firstChunk = chunks[0];
|
||||
const ΔelemC = firstChunk.ΔelemCount;
|
||||
const elemC = firstChunk.elemCount;
|
||||
|
||||
// Check consistency across chunks
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.ΔelemCount !== ΔelemC || chunk.elemCount !== elemC) {
|
||||
throw new Error('Inconsistent chunk structures');
|
||||
}
|
||||
}
|
||||
|
||||
// Get types from first chunk
|
||||
const view = new DataView(firstChunk);
|
||||
const ΔelemBaseTypes = [];
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const typeByte = view.getUint8(12 + k);
|
||||
const baseCode = typeByte & 0xF;
|
||||
const baseType = codeToType[baseCode];
|
||||
if (!baseType) throw new Error('Invalid base type code');
|
||||
ΔelemBaseTypes.push(baseType);
|
||||
}
|
||||
const elemTypes = [];
|
||||
for (let k = 0; k < elemC; k++) {
|
||||
const typeCode = view.getUint8(12 + ΔelemC + k);
|
||||
const type = codeToType[typeCode];
|
||||
if (!type) throw new Error('Invalid elem type code');
|
||||
elemTypes.push(type);
|
||||
}
|
||||
|
||||
// Compute total records
|
||||
const totalN = chunks.reduce((sum, c) => sum + c.jCount, 0);
|
||||
|
||||
// Compute sizes
|
||||
const size_i = totalN * 2; // Uint16 for i
|
||||
const size_j = totalN * 4; // Int32 for j
|
||||
let size_Δelems = 0;
|
||||
for (const t of ΔelemBaseTypes) {
|
||||
size_Δelems += totalN * typeToBytes[t.name];
|
||||
}
|
||||
let size_elems = 0;
|
||||
for (const t of elemTypes) {
|
||||
size_elems += totalN * typeToBytes[t.name];
|
||||
}
|
||||
const totalSize = size_i + size_j + size_Δelems + size_elems;
|
||||
|
||||
const ab = new ArrayBuffer(totalSize);
|
||||
const dv = new DataView(ab);
|
||||
|
||||
// Write i's
|
||||
let off = 0;
|
||||
for (const chunk of chunks) {
|
||||
const i = chunk.i;
|
||||
for (let idx = 0; idx < chunk.jCount; idx++) {
|
||||
dv.setUint16(off, i, true);
|
||||
off += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Write j's
|
||||
off = size_i;
|
||||
for (const chunk of chunks) {
|
||||
const j0 = chunk.j0;
|
||||
const Δj = chunk.Δj;
|
||||
for (let idx = 0; idx < chunk.jCount; idx++) {
|
||||
const j = j0 + idx * Δj;
|
||||
dv.setInt32(off, j, true);
|
||||
off += 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Write Δelems
|
||||
off = size_i + size_j;
|
||||
for (let m = 0; m < ΔelemC; m++) {
|
||||
const type = ΔelemBaseTypes[m];
|
||||
const bytes = typeToBytes[type.name];
|
||||
for (const chunk of chunks) {
|
||||
const arr = chunk.Δelem(m);
|
||||
for (let idx = 0; idx < chunk.jCount; idx++) {
|
||||
writeTypedValue(dv, off, arr[idx], type);
|
||||
off += bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write elems
|
||||
for (let m = 0; m < elemC; m++) {
|
||||
const type = elemTypes[m];
|
||||
const bytes = typeToBytes[type.name];
|
||||
for (const chunk of chunks) {
|
||||
const arr = chunk.elem(m);
|
||||
for (let idx = 0; idx < chunk.jCount; idx++) {
|
||||
writeTypedValue(dv, off, arr[idx], type);
|
||||
off += bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ab;
|
||||
}
|
||||
|
||||
/** Return a ByteArray containing all data from all
|
||||
* chunks including reconstructed i, j and incremental
|
||||
* values, interleaved as follows:
|
||||
*
|
||||
* <i_0> <j_0> <Δelem_0_0> <Δelem_1_0> … <Δelem_y_0> <elem_0_0> <elem_1_0> … <elem_z_0>
|
||||
* <i_1> <j_1> <Δelem_0_1> <Δelem_1_1> … <Δelem_y_1> <elem_0_1> <elem_1_1> … <elem_z_1>
|
||||
* <i_x> <j_x> <Δelem_0_x> <Δelem_1_x> … <Δelem_y_x> <elem_0_x> <elem_1_x> … <elem_z_x>
|
||||
*
|
||||
* It does not matter whether the underlying chunks are
|
||||
* sequential or interleaved. This function will transform
|
||||
* as necessary.
|
||||
*
|
||||
*/
|
||||
getDataInterleaved () {
|
||||
const chunks = this.chunks();
|
||||
if (chunks.length === 0) return new ArrayBuffer(0);
|
||||
|
||||
const firstChunk = chunks[0];
|
||||
const ΔelemC = firstChunk.ΔelemCount;
|
||||
const elemC = firstChunk.elemCount;
|
||||
|
||||
// Check consistency across chunks
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.ΔelemCount !== ΔelemC || chunk.elemCount !== elemC) {
|
||||
throw new Error('Inconsistent chunk structures');
|
||||
}
|
||||
}
|
||||
|
||||
// Get types from first chunk
|
||||
const view = new DataView(firstChunk);
|
||||
const ΔelemBaseTypes = [];
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const typeByte = view.getUint8(12 + k);
|
||||
const baseCode = typeByte & 0xF;
|
||||
const baseType = codeToType[baseCode];
|
||||
if (!baseType) throw new Error('Invalid base type code');
|
||||
ΔelemBaseTypes.push(baseType);
|
||||
}
|
||||
const elemTypes = [];
|
||||
for (let k = 0; k < elemC; k++) {
|
||||
const typeCode = view.getUint8(12 + ΔelemC + k);
|
||||
const type = codeToType[typeCode];
|
||||
if (!type) throw new Error('Invalid elem type code');
|
||||
elemTypes.push(type);
|
||||
}
|
||||
|
||||
// Compute total records
|
||||
const totalN = chunks.reduce((sum, c) => sum + c.jCount, 0);
|
||||
|
||||
// Compute record size
|
||||
const recordSize = 2 + 4 + // i (Uint16) + j (Int32)
|
||||
ΔelemBaseTypes.reduce((sum, t) => sum + typeToBytes[t.name], 0) +
|
||||
elemTypes.reduce((sum, t) => sum + typeToBytes[t.name], 0);
|
||||
const totalSize = totalN * recordSize;
|
||||
|
||||
const ab = new ArrayBuffer(totalSize);
|
||||
const dv = new DataView(ab);
|
||||
|
||||
let off = 0;
|
||||
for (const chunk of chunks) {
|
||||
const i = chunk.i;
|
||||
const j0 = chunk.j0;
|
||||
const Δj = chunk.Δj;
|
||||
for (let idx = 0; idx < chunk.jCount; idx++) {
|
||||
dv.setUint16(off, i, true);
|
||||
off += 2;
|
||||
const j = j0 + idx * Δj;
|
||||
dv.setInt32(off, j, true);
|
||||
off += 4;
|
||||
for (let m = 0; m < ΔelemC; m++) {
|
||||
const type = ΔelemBaseTypes[m];
|
||||
const bytes = typeToBytes[type.name];
|
||||
const arr = chunk.Δelem(m);
|
||||
writeTypedValue(dv, off, arr[idx], type);
|
||||
off += bytes;
|
||||
}
|
||||
for (let m = 0; m < elemC; m++) {
|
||||
const type = elemTypes[m];
|
||||
const bytes = typeToBytes[type.name];
|
||||
const arr = chunk.elem(m);
|
||||
writeTypedValue(dv, off, arr[idx], type);
|
||||
off += bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ab;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class DougalBinaryChunkSequential extends ArrayBuffer {
|
||||
|
||||
constructor (buffer, offset, length) {
|
||||
super(length);
|
||||
new Uint8Array(this).set(new Uint8Array(buffer, offset, length));
|
||||
this._ΔelemCaches = new Array(this.ΔelemCount);
|
||||
this._elemCaches = new Array(this.elemCount);
|
||||
this._ΔelemBlockOffsets = null;
|
||||
this._elemBlockOffsets = null;
|
||||
this._recordOffset = null;
|
||||
}
|
||||
|
||||
_getRecordOffset() {
|
||||
if (this._recordOffset !== null) return this._recordOffset;
|
||||
const view = new DataView(this);
|
||||
const ΔelemC = this.ΔelemCount;
|
||||
const elemC = this.elemCount;
|
||||
|
||||
let recordOffset = 12 + ΔelemC + elemC;
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const tb = view.getUint8(12 + k);
|
||||
const bc = tb & 0xF;
|
||||
const bt = codeToType[bc];
|
||||
recordOffset += typeToBytes[bt.name];
|
||||
}
|
||||
while (recordOffset % 4 !== 0) recordOffset++;
|
||||
this._recordOffset = recordOffset;
|
||||
return recordOffset;
|
||||
}
|
||||
|
||||
_initBlockOffsets() {
|
||||
if (this._ΔelemBlockOffsets !== null) return;
|
||||
const view = new DataView(this);
|
||||
const count = this.jCount;
|
||||
const ΔelemC = this.ΔelemCount;
|
||||
const elemC = this.elemCount;
|
||||
|
||||
const recordOffset = this._getRecordOffset();
|
||||
|
||||
this._ΔelemBlockOffsets = [];
|
||||
let o = recordOffset;
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
this._ΔelemBlockOffsets[k] = o;
|
||||
const tb = view.getUint8(12 + k);
|
||||
const ic = tb >> 4;
|
||||
const it = codeToType[ic];
|
||||
o += count * typeToBytes[it.name];
|
||||
}
|
||||
|
||||
this._elemBlockOffsets = [];
|
||||
for (let k = 0; k < elemC; k++) {
|
||||
this._elemBlockOffsets[k] = o;
|
||||
const tc = view.getUint8(12 + ΔelemC + k);
|
||||
const t = codeToType[tc];
|
||||
o += count * typeToBytes[t.name];
|
||||
}
|
||||
}
|
||||
|
||||
/** Return the user-defined value
|
||||
*/
|
||||
get udv () {
|
||||
return new DataView(this).getUint8(1);
|
||||
}
|
||||
|
||||
/** Return the number of j elements in this chunk
|
||||
*/
|
||||
get jCount () {
|
||||
return new DataView(this).getUint16(2, true);
|
||||
}
|
||||
|
||||
/** Return the i value in this chunk
|
||||
*/
|
||||
get i () {
|
||||
return new DataView(this).getUint16(4, true);
|
||||
}
|
||||
|
||||
/** Return the j0 value in this chunk
|
||||
*/
|
||||
get j0 () {
|
||||
return new DataView(this).getUint16(6, true);
|
||||
}
|
||||
|
||||
/** Return the Δj value in this chunk
|
||||
*/
|
||||
get Δj () {
|
||||
return new DataView(this).getInt16(8, true);
|
||||
}
|
||||
|
||||
/** Return the Δelem_count value in this chunk
|
||||
*/
|
||||
get ΔelemCount () {
|
||||
return new DataView(this).getUint8(10);
|
||||
}
|
||||
|
||||
/** Return the elem_count value in this chunk
|
||||
*/
|
||||
get elemCount () {
|
||||
return new DataView(this).getUint8(11);
|
||||
}
|
||||
|
||||
/** Return a TypedArray (e.g., Uint16Array, …) for the n-th Δelem in the chunk
|
||||
*/
|
||||
Δelem (n) {
|
||||
if (this._ΔelemCaches[n]) return this._ΔelemCaches[n];
|
||||
|
||||
if (n < 0 || n >= this.ΔelemCount) throw new Error(`Invalid Δelem index: ${n}`);
|
||||
const view = new DataView(this);
|
||||
const count = this.jCount;
|
||||
const ΔelemC = this.ΔelemCount;
|
||||
|
||||
const typeByte = view.getUint8(12 + n);
|
||||
const baseCode = typeByte & 0xF;
|
||||
const incrCode = typeByte >> 4;
|
||||
const baseType = codeToType[baseCode];
|
||||
const incrType = codeToType[incrCode];
|
||||
if (!baseType || !incrType) throw new Error('Invalid type codes for Δelem');
|
||||
|
||||
// Find offset for initial value of this Δelem
|
||||
let initialOffset = 12 + ΔelemC + this.elemCount;
|
||||
for (let k = 0; k < n; k++) {
|
||||
const tb = view.getUint8(12 + k);
|
||||
const bc = tb & 0xF;
|
||||
const bt = codeToType[bc];
|
||||
initialOffset += typeToBytes[bt.name];
|
||||
}
|
||||
|
||||
let current = readTypedValue(view, initialOffset, baseType);
|
||||
|
||||
// Advance to start of record data (after all initials and pad)
|
||||
const recordOffset = this._getRecordOffset();
|
||||
|
||||
// Find offset for deltas of this Δelem (skip previous Δelems' delta blocks)
|
||||
this._initBlockOffsets();
|
||||
const deltaOffset = this._ΔelemBlockOffsets[n];
|
||||
|
||||
// Reconstruct the array
|
||||
const arr = new baseType(count);
|
||||
const isBigInt = baseType === BigInt64Array || baseType === BigUint64Array;
|
||||
arr[0] = current;
|
||||
for (let idx = 1; idx < count; idx++) {
|
||||
let delta = readTypedValue(view, deltaOffset + idx * typeToBytes[incrType.name], incrType);
|
||||
if (isBigInt) {
|
||||
delta = BigInt(delta);
|
||||
current += delta;
|
||||
} else {
|
||||
current += delta;
|
||||
}
|
||||
arr[idx] = current;
|
||||
}
|
||||
|
||||
this._ΔelemCaches[n] = arr;
|
||||
return arr;
|
||||
}
|
||||
|
||||
/** Return a TypedArray (e.g., Uint16Array, …) for the n-th elem in the chunk
|
||||
*/
|
||||
elem (n) {
|
||||
if (this._elemCaches[n]) return this._elemCaches[n];
|
||||
|
||||
if (n < 0 || n >= this.elemCount) throw new Error(`Invalid elem index: ${n}`);
|
||||
const view = new DataView(this);
|
||||
const count = this.jCount;
|
||||
const ΔelemC = this.ΔelemCount;
|
||||
const elemC = this.elemCount;
|
||||
|
||||
const typeCode = view.getUint8(12 + ΔelemC + n);
|
||||
const type = codeToType[typeCode];
|
||||
if (!type) throw new Error('Invalid type code for elem');
|
||||
|
||||
// Find offset for this elem's data block
|
||||
this._initBlockOffsets();
|
||||
const elemOffset = this._elemBlockOffsets[n];
|
||||
|
||||
// Create and populate the array
|
||||
const arr = new type(count);
|
||||
const bytes = typeToBytes[type.name];
|
||||
for (let idx = 0; idx < count; idx++) {
|
||||
arr[idx] = readTypedValue(view, elemOffset + idx * bytes, type);
|
||||
}
|
||||
|
||||
this._elemCaches[n] = arr;
|
||||
return arr;
|
||||
}
|
||||
|
||||
getRecord (index) {
|
||||
if (index < 0 || index >= this.jCount) throw new Error(`Invalid record index: ${index}`);
|
||||
|
||||
const arr = [this.udv, this.i, this.j0 + index * this.Δj];
|
||||
|
||||
for (let m = 0; m < this.ΔelemCount; m++) {
|
||||
const values = this.Δelem(m);
|
||||
arr.push(values[index]);
|
||||
}
|
||||
|
||||
for (let m = 0; m < this.elemCount; m++) {
|
||||
const values = this.elem(m);
|
||||
arr.push(values[index]);
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DougalBinaryChunkInterleaved extends ArrayBuffer {
|
||||
constructor(buffer, offset, length) {
|
||||
super(length);
|
||||
new Uint8Array(this).set(new Uint8Array(buffer, offset, length));
|
||||
this._incrStrides = [];
|
||||
this._elemStrides = [];
|
||||
this._incrOffsets = [];
|
||||
this._elemOffsets = [];
|
||||
this._recordStride = 0;
|
||||
this._recordOffset = null;
|
||||
this._initStrides();
|
||||
this._ΔelemCaches = new Array(this.ΔelemCount);
|
||||
this._elemCaches = new Array(this.elemCount);
|
||||
}
|
||||
|
||||
_getRecordOffset() {
|
||||
if (this._recordOffset !== null) return this._recordOffset;
|
||||
const view = new DataView(this);
|
||||
const ΔelemC = this.ΔelemCount;
|
||||
const elemC = this.elemCount;
|
||||
|
||||
let recordOffset = 12 + ΔelemC + elemC;
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const tb = view.getUint8(12 + k);
|
||||
const bc = tb & 0xF;
|
||||
const bt = codeToType[bc];
|
||||
recordOffset += typeToBytes[bt.name];
|
||||
}
|
||||
while (recordOffset % 4 !== 0) recordOffset++;
|
||||
this._recordOffset = recordOffset;
|
||||
return recordOffset;
|
||||
}
|
||||
|
||||
_initStrides() {
|
||||
const view = new DataView(this);
|
||||
const ΔelemC = this.ΔelemCount;
|
||||
const elemC = this.elemCount;
|
||||
|
||||
// Compute incr strides and offsets
|
||||
let incrOffset = 0;
|
||||
for (let k = 0; k < ΔelemC; k++) {
|
||||
const typeByte = view.getUint8(12 + k);
|
||||
const incrCode = typeByte >> 4;
|
||||
const incrType = codeToType[incrCode];
|
||||
if (!incrType) throw new Error('Invalid incr type code');
|
||||
this._incrOffsets.push(incrOffset);
|
||||
const bytes = typeToBytes[incrType.name];
|
||||
this._incrStrides.push(bytes);
|
||||
incrOffset += bytes;
|
||||
this._recordStride += bytes;
|
||||
}
|
||||
|
||||
// Compute elem strides and offsets
|
||||
let elemOffset = incrOffset;
|
||||
for (let k = 0; k < elemC; k++) {
|
||||
const typeCode = view.getUint8(12 + ΔelemC + k);
|
||||
const type = codeToType[typeCode];
|
||||
if (!type) throw new Error('Invalid elem type code');
|
||||
this._elemOffsets.push(elemOffset);
|
||||
const bytes = typeToBytes[type.name];
|
||||
this._elemStrides.push(bytes);
|
||||
elemOffset += bytes;
|
||||
this._recordStride += bytes;
|
||||
}
|
||||
}
|
||||
|
||||
get udv() {
|
||||
return new DataView(this).getUint8(1);
|
||||
}
|
||||
|
||||
get jCount() {
|
||||
return new DataView(this).getUint16(2, true);
|
||||
}
|
||||
|
||||
get i() {
|
||||
return new DataView(this).getUint16(4, true);
|
||||
}
|
||||
|
||||
get j0() {
|
||||
return new DataView(this).getUint16(6, true);
|
||||
}
|
||||
|
||||
get Δj() {
|
||||
return new DataView(this).getInt16(8, true);
|
||||
}
|
||||
|
||||
get ΔelemCount() {
|
||||
return new DataView(this).getUint8(10);
|
||||
}
|
||||
|
||||
get elemCount() {
|
||||
return new DataView(this).getUint8(11);
|
||||
}
|
||||
|
||||
Δelem(n) {
|
||||
if (this._ΔelemCaches[n]) return this._ΔelemCaches[n];
|
||||
|
||||
if (n < 0 || n >= this.ΔelemCount) throw new Error(`Invalid Δelem index: ${n}`);
|
||||
const view = new DataView(this);
|
||||
const count = this.jCount;
|
||||
const ΔelemC = this.ΔelemCount;
|
||||
|
||||
const typeByte = view.getUint8(12 + n);
|
||||
const baseCode = typeByte & 0xF;
|
||||
const incrCode = typeByte >> 4;
|
||||
const baseType = codeToType[baseCode];
|
||||
const incrType = codeToType[incrCode];
|
||||
if (!baseType || !incrType) throw new Error('Invalid type codes for Δelem');
|
||||
|
||||
// Find offset for initial value of this Δelem
|
||||
let initialOffset = 12 + ΔelemC + this.elemCount;
|
||||
for (let k = 0; k < n; k++) {
|
||||
const tb = view.getUint8(12 + k);
|
||||
const bc = tb & 0xF;
|
||||
const bt = codeToType[bc];
|
||||
initialOffset += typeToBytes[bt.name];
|
||||
}
|
||||
|
||||
let current = readTypedValue(view, initialOffset, baseType);
|
||||
|
||||
// Find offset to start of record data
|
||||
const recordOffset = this._getRecordOffset();
|
||||
|
||||
// Use precomputed offset for this Δelem
|
||||
const deltaOffset = recordOffset + this._incrOffsets[n];
|
||||
|
||||
// Reconstruct the array
|
||||
const arr = new baseType(count);
|
||||
const isBigInt = baseType === BigInt64Array || baseType === BigUint64Array;
|
||||
arr[0] = current;
|
||||
for (let idx = 1; idx < count; idx++) {
|
||||
let delta = readTypedValue(view, deltaOffset + idx * this._recordStride, incrType);
|
||||
if (isBigInt) {
|
||||
delta = BigInt(delta);
|
||||
current += delta;
|
||||
} else {
|
||||
current += delta;
|
||||
}
|
||||
arr[idx] = current;
|
||||
}
|
||||
|
||||
this._ΔelemCaches[n] = arr;
|
||||
return arr;
|
||||
}
|
||||
|
||||
elem(n) {
|
||||
if (this._elemCaches[n]) return this._elemCaches[n];
|
||||
|
||||
if (n < 0 || n >= this.elemCount) throw new Error(`Invalid elem index: ${n}`);
|
||||
const view = new DataView(this);
|
||||
const count = this.jCount;
|
||||
const ΔelemC = this.ΔelemCount;
|
||||
|
||||
const typeCode = view.getUint8(12 + ΔelemC + n);
|
||||
const type = codeToType[typeCode];
|
||||
if (!type) throw new Error('Invalid type code for elem');
|
||||
|
||||
// Find offset to start of record data
|
||||
const recordOffset = this._getRecordOffset();
|
||||
|
||||
// Use precomputed offset for this elem (relative to start of record data)
|
||||
const elemOffset = recordOffset + this._elemOffsets[n];
|
||||
|
||||
// Create and populate the array
|
||||
const arr = new type(count);
|
||||
const bytes = typeToBytes[type.name];
|
||||
for (let idx = 0; idx < count; idx++) {
|
||||
arr[idx] = readTypedValue(view, elemOffset + idx * this._recordStride, type);
|
||||
}
|
||||
|
||||
this._elemCaches[n] = arr;
|
||||
return arr;
|
||||
}
|
||||
|
||||
getRecord (index) {
|
||||
if (index < 0 || index >= this.jCount) throw new Error(`Invalid record index: ${index}`);
|
||||
|
||||
const arr = [this.udv, this.i, this.j0 + index * this.Δj];
|
||||
|
||||
for (let m = 0; m < this.ΔelemCount; m++) {
|
||||
const values = this.Δelem(m);
|
||||
arr.push(values[index]);
|
||||
}
|
||||
|
||||
for (let m = 0; m < this.elemCount; m++) {
|
||||
const values = this.elem(m);
|
||||
arr.push(values[index]);
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved }
|
||||
327
lib/modules/@dougal/binary/decode.js
Normal file
327
lib/modules/@dougal/binary/decode.js
Normal file
@@ -0,0 +1,327 @@
|
||||
const codeToType = {
|
||||
0: Int8Array,
|
||||
1: Uint8Array,
|
||||
2: Int16Array,
|
||||
3: Uint16Array,
|
||||
4: Int32Array,
|
||||
5: Uint32Array,
|
||||
7: Float32Array,
|
||||
8: Float64Array,
|
||||
9: BigInt64Array,
|
||||
10: BigUint64Array
|
||||
};
|
||||
|
||||
const typeToBytes = {
|
||||
Int8Array: 1,
|
||||
Uint8Array: 1,
|
||||
Int16Array: 2,
|
||||
Uint16Array: 2,
|
||||
Int32Array: 4,
|
||||
Uint32Array: 4,
|
||||
Float32Array: 4,
|
||||
Float64Array: 8,
|
||||
BigInt64Array: 8,
|
||||
BigUint64Array: 8
|
||||
};
|
||||
|
||||
function sequential(binary) {
|
||||
if (!(binary instanceof Uint8Array) || binary.length < 4) {
|
||||
throw new Error('Invalid binary input');
|
||||
}
|
||||
|
||||
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength);
|
||||
let offset = 0;
|
||||
|
||||
// Initialize result (assuming single i value for simplicity; extend for multiple i values if needed)
|
||||
const result = { i: null, j: [], Δelems: [], elems: [] };
|
||||
|
||||
// Process bundles
|
||||
while (offset < binary.length) {
|
||||
// Read bundle header
|
||||
if (offset + 4 > binary.length) throw new Error('Incomplete bundle header');
|
||||
|
||||
const bundleHeader = view.getUint32(offset, true);
|
||||
if ((bundleHeader & 0xFF) !== 0x1C) throw new Error('Invalid bundle marker');
|
||||
const bundleLength = bundleHeader >> 8;
|
||||
offset += 4;
|
||||
const bundleEnd = offset + bundleLength;
|
||||
|
||||
if (bundleEnd > binary.length) throw new Error('Bundle length exceeds input size');
|
||||
|
||||
// Process chunks in bundle
|
||||
while (offset < bundleEnd) {
|
||||
// Read chunk header
|
||||
if (offset + 12 > bundleEnd) throw new Error('Incomplete chunk header');
|
||||
const chunkType = view.getUint8(offset);
|
||||
if (chunkType !== 0x11) throw new Error(`Unsupported chunk type: ${chunkType}`);
|
||||
offset += 1; // Skip udv
|
||||
offset += 1;
|
||||
const count = view.getUint16(offset, true); offset += 2;
|
||||
if (count > 65535) throw new Error('Chunk count exceeds 65535');
|
||||
const iValue = view.getUint16(offset, true); offset += 2;
|
||||
const j0 = view.getUint16(offset, true); offset += 2;
|
||||
const Δj = view.getInt16(offset, true); offset += 2;
|
||||
const ΔelemCount = view.getUint8(offset++); // Δelem_count
|
||||
const elemCount = view.getUint8(offset++); // elem_count
|
||||
|
||||
// Set i value (assuming all chunks share the same i)
|
||||
if (result.i === null) result.i = iValue;
|
||||
else if (result.i !== iValue) throw new Error('Multiple i values not supported');
|
||||
|
||||
// Read preface (element types)
|
||||
const ΔelemTypes = [];
|
||||
for (let i = 0; i < ΔelemCount; i++) {
|
||||
if (offset >= bundleEnd) throw new Error('Incomplete Δelem types');
|
||||
const typeByte = view.getUint8(offset++);
|
||||
const baseCode = typeByte & 0x0F;
|
||||
const incrCode = typeByte >> 4;
|
||||
if (!codeToType[baseCode] || !codeToType[incrCode]) {
|
||||
throw new Error(`Invalid type code in Δelem: ${typeByte}`);
|
||||
}
|
||||
ΔelemTypes.push({ baseType: codeToType[baseCode], incrType: codeToType[incrCode] });
|
||||
}
|
||||
const elemTypes = [];
|
||||
for (let i = 0; i < elemCount; i++) {
|
||||
if (offset >= bundleEnd) throw new Error('Incomplete elem types');
|
||||
const typeCode = view.getUint8(offset++);
|
||||
if (!codeToType[typeCode]) throw new Error(`Invalid type code in elem: ${typeCode}`);
|
||||
elemTypes.push(codeToType[typeCode]);
|
||||
}
|
||||
|
||||
// Initialize Δelems and elems arrays if first chunk
|
||||
if (!result.Δelems.length && ΔelemCount > 0) {
|
||||
result.Δelems = Array(ΔelemCount).fill().map(() => []);
|
||||
}
|
||||
if (!result.elems.length && elemCount > 0) {
|
||||
result.elems = Array(elemCount).fill().map(() => []);
|
||||
}
|
||||
|
||||
// Read initial values for Δelems
|
||||
const initialValues = [];
|
||||
for (const { baseType } of ΔelemTypes) {
|
||||
if (offset + typeToBytes[baseType.name] > bundleEnd) {
|
||||
throw new Error('Incomplete initial values');
|
||||
}
|
||||
initialValues.push(readTypedValue(view, offset, baseType));
|
||||
offset += typeToBytes[baseType.name];
|
||||
}
|
||||
// Skip padding
|
||||
while (offset % 4 !== 0) {
|
||||
if (offset >= bundleEnd) throw new Error('Incomplete padding after initial values');
|
||||
offset++;
|
||||
}
|
||||
|
||||
// Reconstruct j values
|
||||
for (let idx = 0; idx < count; idx++) {
|
||||
result.j.push(j0 + idx * Δj);
|
||||
}
|
||||
|
||||
// Read record data (non-interleaved)
|
||||
for (let i = 0; i < ΔelemCount; i++) {
|
||||
let current = initialValues[i];
|
||||
const values = result.Δelems[i];
|
||||
const incrType = ΔelemTypes[i].incrType;
|
||||
const isBigInt = typeof current === 'bigint';
|
||||
for (let idx = 0; idx < count; idx++) {
|
||||
if (offset + typeToBytes[incrType.name] > bundleEnd) {
|
||||
throw new Error('Incomplete Δelem data');
|
||||
}
|
||||
let delta = readTypedValue(view, offset, incrType);
|
||||
if (idx === 0) {
|
||||
values.push(isBigInt ? Number(current) : current);
|
||||
} else {
|
||||
if (isBigInt) {
|
||||
delta = BigInt(delta);
|
||||
current += delta;
|
||||
values.push(Number(current));
|
||||
} else {
|
||||
current += delta;
|
||||
values.push(current);
|
||||
}
|
||||
}
|
||||
offset += typeToBytes[incrType.name];
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < elemCount; i++) {
|
||||
const values = result.elems[i];
|
||||
const type = elemTypes[i];
|
||||
const isBigInt = type === BigInt64Array || type === BigUint64Array;
|
||||
for (let idx = 0; idx < count; idx++) {
|
||||
if (offset + typeToBytes[type.name] > bundleEnd) {
|
||||
throw new Error('Incomplete elem data');
|
||||
}
|
||||
let value = readTypedValue(view, offset, type);
|
||||
values.push(isBigInt ? Number(value) : value);
|
||||
offset += typeToBytes[type.name];
|
||||
}
|
||||
}
|
||||
// Skip padding
|
||||
while (offset % 4 !== 0) {
|
||||
if (offset >= bundleEnd) throw new Error('Incomplete padding after record data');
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function interleaved(binary) {
|
||||
if (!(binary instanceof Uint8Array) || binary.length < 4) {
|
||||
throw new Error('Invalid binary input');
|
||||
}
|
||||
|
||||
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength);
|
||||
let offset = 0;
|
||||
|
||||
// Initialize result (assuming single i value for simplicity; extend for multiple i values if needed)
|
||||
const result = { i: null, j: [], Δelems: [], elems: [] };
|
||||
|
||||
// Process bundles
|
||||
while (offset < binary.length) {
|
||||
// Read bundle header
|
||||
if (offset + 4 > binary.length) throw new Error('Incomplete bundle header');
|
||||
|
||||
const bundleHeader = view.getUint32(offset, true);
|
||||
if ((bundleHeader & 0xFF) !== 0x1C) throw new Error('Invalid bundle marker');
|
||||
const bundleLength = bundleHeader >> 8;
|
||||
offset += 4;
|
||||
const bundleEnd = offset + bundleLength;
|
||||
|
||||
if (bundleEnd > binary.length) throw new Error('Bundle length exceeds input size');
|
||||
|
||||
// Process chunks in bundle
|
||||
while (offset < bundleEnd) {
|
||||
// Read chunk header
|
||||
if (offset + 12 > bundleEnd) throw new Error('Incomplete chunk header');
|
||||
const chunkType = view.getUint8(offset);
|
||||
if (chunkType !== 0x12) throw new Error(`Unsupported chunk type: ${chunkType}`);
|
||||
offset += 1; // Skip udv
|
||||
offset += 1;
|
||||
const count = view.getUint16(offset, true); offset += 2;
|
||||
if (count > 65535) throw new Error('Chunk count exceeds 65535');
|
||||
const iValue = view.getUint16(offset, true); offset += 2;
|
||||
const j0 = view.getUint16(offset, true); offset += 2;
|
||||
const Δj = view.getInt16(offset, true); offset += 2;
|
||||
const ΔelemCount = view.getUint8(offset++); // Δelem_count
|
||||
const elemCount = view.getUint8(offset++); // elem_count
|
||||
|
||||
// Set i value (assuming all chunks share the same i)
|
||||
if (result.i === null) result.i = iValue;
|
||||
else if (result.i !== iValue) throw new Error('Multiple i values not supported');
|
||||
|
||||
// Read preface (element types)
|
||||
const ΔelemTypes = [];
|
||||
for (let i = 0; i < ΔelemCount; i++) {
|
||||
if (offset >= bundleEnd) throw new Error('Incomplete Δelem types');
|
||||
const typeByte = view.getUint8(offset++);
|
||||
const baseCode = typeByte & 0x0F;
|
||||
const incrCode = typeByte >> 4;
|
||||
if (!codeToType[baseCode] || !codeToType[incrCode]) {
|
||||
throw new Error(`Invalid type code in Δelem: ${typeByte}`);
|
||||
}
|
||||
ΔelemTypes.push({ baseType: codeToType[baseCode], incrType: codeToType[incrCode] });
|
||||
}
|
||||
const elemTypes = [];
|
||||
for (let i = 0; i < elemCount; i++) {
|
||||
if (offset >= bundleEnd) throw new Error('Incomplete elem types');
|
||||
const typeCode = view.getUint8(offset++);
|
||||
if (!codeToType[typeCode]) throw new Error(`Invalid type code in elem: ${typeCode}`);
|
||||
elemTypes.push(codeToType[typeCode]);
|
||||
}
|
||||
|
||||
// Initialize Δelems and elems arrays if first chunk
|
||||
if (!result.Δelems.length && ΔelemCount > 0) {
|
||||
result.Δelems = Array(ΔelemCount).fill().map(() => []);
|
||||
}
|
||||
if (!result.elems.length && elemCount > 0) {
|
||||
result.elems = Array(elemCount).fill().map(() => []);
|
||||
}
|
||||
|
||||
// Read initial values for Δelems
|
||||
const initialValues = [];
|
||||
for (const { baseType } of ΔelemTypes) {
|
||||
if (offset + typeToBytes[baseType.name] > bundleEnd) {
|
||||
throw new Error('Incomplete initial values');
|
||||
}
|
||||
initialValues.push(readTypedValue(view, offset, baseType));
|
||||
offset += typeToBytes[baseType.name];
|
||||
}
|
||||
// Skip padding
|
||||
while (offset % 4 !== 0) {
|
||||
if (offset >= bundleEnd) throw new Error('Incomplete padding after initial values');
|
||||
offset++;
|
||||
}
|
||||
|
||||
// Reconstruct j values
|
||||
for (let idx = 0; idx < count; idx++) {
|
||||
result.j.push(j0 + idx * Δj);
|
||||
}
|
||||
|
||||
// Read interleaved record data
|
||||
for (let idx = 0; idx < count; idx++) {
|
||||
// Read Δelems
|
||||
for (let i = 0; i < ΔelemCount; i++) {
|
||||
const values = result.Δelems[i];
|
||||
const incrType = ΔelemTypes[i].incrType;
|
||||
const isBigInt = typeof initialValues[i] === 'bigint';
|
||||
if (offset + typeToBytes[incrType.name] > bundleEnd) {
|
||||
throw new Error('Incomplete Δelem data');
|
||||
}
|
||||
let delta = readTypedValue(view, offset, incrType);
|
||||
offset += typeToBytes[incrType.name];
|
||||
if (idx === 0) {
|
||||
values.push(isBigInt ? Number(initialValues[i]) : initialValues[i]);
|
||||
} else {
|
||||
if (isBigInt) {
|
||||
delta = BigInt(delta);
|
||||
initialValues[i] += delta;
|
||||
values.push(Number(initialValues[i]));
|
||||
} else {
|
||||
initialValues[i] += delta;
|
||||
values.push(initialValues[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Read elems
|
||||
for (let i = 0; i < elemCount; i++) {
|
||||
const values = result.elems[i];
|
||||
const type = elemTypes[i];
|
||||
const isBigInt = type === BigInt64Array || type === BigUint64Array;
|
||||
if (offset + typeToBytes[type.name] > bundleEnd) {
|
||||
throw new Error('Incomplete elem data');
|
||||
}
|
||||
let value = readTypedValue(view, offset, type);
|
||||
values.push(isBigInt ? Number(value) : value);
|
||||
offset += typeToBytes[type.name];
|
||||
}
|
||||
}
|
||||
// Skip padding
|
||||
while (offset % 4 !== 0) {
|
||||
if (offset >= bundleEnd) throw new Error('Incomplete padding after record data');
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function readTypedValue(view, offset, type) {
|
||||
switch (type) {
|
||||
case Int8Array: return view.getInt8(offset);
|
||||
case Uint8Array: return view.getUint8(offset);
|
||||
case Int16Array: return view.getInt16(offset, true);
|
||||
case Uint16Array: return view.getUint16(offset, true);
|
||||
case Int32Array: return view.getInt32(offset, true);
|
||||
case Uint32Array: return view.getUint32(offset, true);
|
||||
case Float32Array: return view.getFloat32(offset, true);
|
||||
case Float64Array: return view.getFloat64(offset, true);
|
||||
case BigInt64Array: return view.getBigInt64(offset, true);
|
||||
case BigUint64Array: return view.getBigUint64(offset, true);
|
||||
default: throw new Error(`Unsupported type: ${type.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { sequential, interleaved };
|
||||
380
lib/modules/@dougal/binary/encode.js
Normal file
380
lib/modules/@dougal/binary/encode.js
Normal file
@@ -0,0 +1,380 @@
|
||||
const typeToCode = {
|
||||
Int8Array: 0,
|
||||
Uint8Array: 1,
|
||||
Int16Array: 2,
|
||||
Uint16Array: 3,
|
||||
Int32Array: 4,
|
||||
Uint32Array: 5,
|
||||
Float32Array: 7, // Float16 not natively supported in JS, use Float32
|
||||
Float64Array: 8,
|
||||
BigInt64Array: 9,
|
||||
BigUint64Array: 10
|
||||
};
|
||||
|
||||
const typeToBytes = {
|
||||
Int8Array: 1,
|
||||
Uint8Array: 1,
|
||||
Int16Array: 2,
|
||||
Uint16Array: 2,
|
||||
Int32Array: 4,
|
||||
Uint32Array: 4,
|
||||
Float32Array: 4,
|
||||
Float64Array: 8,
|
||||
BigInt64Array: 8,
|
||||
BigUint64Array: 8
|
||||
};
|
||||
|
||||
function sequential(json, iGetter, jGetter, Δelems = [], elems = [], udv = 0) {
|
||||
if (!Array.isArray(json) || !json.length) return new Uint8Array(0);
|
||||
if (typeof iGetter !== 'function' || typeof jGetter !== 'function') throw new Error('i and j must be getter functions');
|
||||
Δelems.forEach((elem, idx) => {
|
||||
if (typeof elem.key !== 'function') throw new Error(`Δelems[${idx}].key must be a getter function`);
|
||||
});
|
||||
elems.forEach((elem, idx) => {
|
||||
if (typeof elem.key !== 'function') throw new Error(`elems[${idx}].key must be a getter function`);
|
||||
});
|
||||
|
||||
// Group records by i value
|
||||
const groups = new Map();
|
||||
for (const record of json) {
|
||||
const iValue = iGetter(record);
|
||||
if (iValue == null) throw new Error('Missing i value from getter');
|
||||
if (!groups.has(iValue)) groups.set(iValue, []);
|
||||
groups.get(iValue).push(record);
|
||||
}
|
||||
|
||||
const maxBundleSize = 0xFFFFFF; // Max bundle length (24 bits)
|
||||
const buffers = [];
|
||||
|
||||
// Process each group (i value)
|
||||
for (const [iValue, records] of groups) {
|
||||
// Sort records by j to ensure consistent order
|
||||
records.sort((a, b) => jGetter(a) - jGetter(b));
|
||||
const jValues = records.map(jGetter);
|
||||
if (jValues.some(v => v == null)) throw new Error('Missing j value from getter');
|
||||
|
||||
// Split records into chunks based on Δj continuity
|
||||
const chunks = [];
|
||||
let currentChunk = [records[0]];
|
||||
let currentJ0 = jValues[0];
|
||||
let currentΔj = records.length > 1 ? jValues[1] - jValues[0] : 0;
|
||||
|
||||
for (let idx = 1; idx < records.length; idx++) {
|
||||
const chunkIndex = chunks.reduce((sum, c) => sum + c.records.length, 0);
|
||||
const expectedJ = currentJ0 + (idx - chunkIndex) * currentΔj;
|
||||
if (jValues[idx] !== expectedJ || idx - chunkIndex >= 65536) {
|
||||
chunks.push({ records: currentChunk, j0: currentJ0, Δj: currentΔj });
|
||||
currentChunk = [records[idx]];
|
||||
currentJ0 = jValues[idx];
|
||||
currentΔj = idx + 1 < records.length ? jValues[idx + 1] - jValues[idx] : 0;
|
||||
} else {
|
||||
currentChunk.push(records[idx]);
|
||||
}
|
||||
}
|
||||
if (currentChunk.length > 0) {
|
||||
chunks.push({ records: currentChunk, j0: currentJ0, Δj: currentΔj });
|
||||
}
|
||||
|
||||
// Calculate total size for all chunks in this group by simulating offsets
|
||||
const chunkSizes = chunks.map(({ records: chunkRecords }) => {
|
||||
if (chunkRecords.length > 65535) throw new Error(`Chunk size exceeds 65535 for i=${iValue}`);
|
||||
let simulatedOffset = 0; // Relative to chunk start
|
||||
simulatedOffset += 12; // Header
|
||||
simulatedOffset += Δelems.length + elems.length; // Preface
|
||||
simulatedOffset += Δelems.reduce((sum, e) => sum + typeToBytes[e.baseType.name], 0); // Initial values
|
||||
while (simulatedOffset % 4 !== 0) simulatedOffset++; // Pad after initial
|
||||
simulatedOffset += chunkRecords.length * (
|
||||
Δelems.reduce((sum, e) => sum + typeToBytes[e.incrType.name], 0) +
|
||||
elems.reduce((sum, e) => sum + typeToBytes[e.type.name], 0)
|
||||
); // Record data
|
||||
while (simulatedOffset % 4 !== 0) simulatedOffset++; // Pad after record
|
||||
return simulatedOffset;
|
||||
});
|
||||
const totalChunkSize = chunkSizes.reduce((sum, size) => sum + size, 0);
|
||||
|
||||
// Start a new bundle if needed
|
||||
const lastBundle = buffers[buffers.length - 1];
|
||||
if (!lastBundle || lastBundle.offset + totalChunkSize > maxBundleSize) {
|
||||
buffers.push({ offset: 4, buffer: null, view: null });
|
||||
}
|
||||
|
||||
// Initialize DataView for current bundle
|
||||
const currentBundle = buffers[buffers.length - 1];
|
||||
if (!currentBundle.buffer) {
|
||||
const requiredSize = totalChunkSize + 4;
|
||||
currentBundle.buffer = new ArrayBuffer(requiredSize);
|
||||
currentBundle.view = new DataView(currentBundle.buffer);
|
||||
}
|
||||
|
||||
// Process each chunk
|
||||
for (const { records: chunkRecords, j0, Δj } of chunks) {
|
||||
const chunkSize = chunkSizes.shift();
|
||||
|
||||
// Ensure buffer is large enough
|
||||
if (currentBundle.offset + chunkSize > currentBundle.buffer.byteLength) {
|
||||
const newSize = currentBundle.offset + chunkSize;
|
||||
const newBuffer = new ArrayBuffer(newSize);
|
||||
new Uint8Array(newBuffer).set(new Uint8Array(currentBundle.buffer));
|
||||
currentBundle.buffer = newBuffer;
|
||||
currentBundle.view = new DataView(newBuffer);
|
||||
}
|
||||
|
||||
// Write chunk header
|
||||
let offset = currentBundle.offset;
|
||||
currentBundle.view.setUint8(offset++, 0x11); // Chunk type
|
||||
currentBundle.view.setUint8(offset++, udv); // udv
|
||||
currentBundle.view.setUint16(offset, chunkRecords.length, true); offset += 2; // count
|
||||
currentBundle.view.setUint16(offset, iValue, true); offset += 2; // i
|
||||
currentBundle.view.setUint16(offset, j0, true); offset += 2; // j0
|
||||
currentBundle.view.setInt16(offset, Δj, true); offset += 2; // Δj
|
||||
currentBundle.view.setUint8(offset++, Δelems.length); // Δelem_count
|
||||
currentBundle.view.setUint8(offset++, elems.length); // elem_count
|
||||
|
||||
// Write chunk preface (element types)
|
||||
for (const elem of Δelems) {
|
||||
const baseCode = typeToCode[elem.baseType.name];
|
||||
const incrCode = typeToCode[elem.incrType.name];
|
||||
currentBundle.view.setUint8(offset++, (incrCode << 4) | baseCode);
|
||||
}
|
||||
for (const elem of elems) {
|
||||
currentBundle.view.setUint8(offset++, typeToCode[elem.type.name]);
|
||||
}
|
||||
|
||||
// Write initial values for Δelems
|
||||
for (const elem of Δelems) {
|
||||
const value = elem.key(chunkRecords[0]);
|
||||
if (value == null) throw new Error('Missing Δelem value from getter');
|
||||
writeTypedValue(currentBundle.view, offset, value, elem.baseType);
|
||||
offset += typeToBytes[elem.baseType.name];
|
||||
}
|
||||
// Pad to 4-byte boundary
|
||||
while (offset % 4 !== 0) currentBundle.view.setUint8(offset++, 0);
|
||||
|
||||
// Write record data (non-interleaved)
|
||||
for (const elem of Δelems) {
|
||||
let prev = elem.key(chunkRecords[0]);
|
||||
for (let idx = 0; idx < chunkRecords.length; idx++) {
|
||||
const value = idx === 0 ? 0 : elem.key(chunkRecords[idx]) - prev;
|
||||
writeTypedValue(currentBundle.view, offset, value, elem.incrType);
|
||||
offset += typeToBytes[elem.incrType.name];
|
||||
prev = elem.key(chunkRecords[idx]);
|
||||
}
|
||||
}
|
||||
for (const elem of elems) {
|
||||
for (const record of chunkRecords) {
|
||||
const value = elem.key(record);
|
||||
if (value == null) throw new Error('Missing elem value from getter');
|
||||
writeTypedValue(currentBundle.view, offset, value, elem.type);
|
||||
offset += typeToBytes[elem.type.name];
|
||||
}
|
||||
}
|
||||
// Pad to 4-byte boundary
|
||||
while (offset % 4 !== 0) currentBundle.view.setUint8(offset++, 0);
|
||||
|
||||
// Update bundle offset
|
||||
currentBundle.offset = offset;
|
||||
}
|
||||
|
||||
// Update bundle header
|
||||
currentBundle.view.setUint32(0, 0x1C | ((currentBundle.offset - 4) << 8), true);
|
||||
}
|
||||
|
||||
// Combine buffers into final Uint8Array
|
||||
const finalLength = buffers.reduce((sum, b) => sum + b.offset, 0);
|
||||
const result = new Uint8Array(finalLength);
|
||||
let offset = 0;
|
||||
for (const { buffer, offset: bundleOffset } of buffers) {
|
||||
result.set(new Uint8Array(buffer, 0, bundleOffset), offset);
|
||||
offset += bundleOffset;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function interleaved(json, iGetter, jGetter, Δelems = [], elems = [], udv = 0) {
|
||||
if (!Array.isArray(json) || !json.length) return new Uint8Array(0);
|
||||
if (typeof iGetter !== 'function' || typeof jGetter !== 'function') throw new Error('i and j must be getter functions');
|
||||
Δelems.forEach((elem, idx) => {
|
||||
if (typeof elem.key !== 'function') throw new Error(`Δelems[${idx}].key must be a getter function`);
|
||||
});
|
||||
elems.forEach((elem, idx) => {
|
||||
if (typeof elem.key !== 'function') throw new Error(`elems[${idx}].key must be a getter function`);
|
||||
});
|
||||
|
||||
// Group records by i value
|
||||
const groups = new Map();
|
||||
for (const record of json) {
|
||||
const iValue = iGetter(record);
|
||||
if (iValue == null) throw new Error('Missing i value from getter');
|
||||
if (!groups.has(iValue)) groups.set(iValue, []);
|
||||
groups.get(iValue).push(record);
|
||||
}
|
||||
|
||||
const maxBundleSize = 0xFFFFFF; // Max bundle length (24 bits)
|
||||
const buffers = [];
|
||||
|
||||
// Process each group (i value)
|
||||
for (const [iValue, records] of groups) {
|
||||
// Sort records by j to ensure consistent order
|
||||
records.sort((a, b) => jGetter(a) - jGetter(b));
|
||||
const jValues = records.map(jGetter);
|
||||
if (jValues.some(v => v == null)) throw new Error('Missing j value from getter');
|
||||
|
||||
// Split records into chunks based on Δj continuity
|
||||
const chunks = [];
|
||||
let currentChunk = [records[0]];
|
||||
let currentJ0 = jValues[0];
|
||||
let currentΔj = records.length > 1 ? jValues[1] - jValues[0] : 0;
|
||||
|
||||
for (let idx = 1; idx < records.length; idx++) {
|
||||
const chunkIndex = chunks.reduce((sum, c) => sum + c.records.length, 0);
|
||||
const expectedJ = currentJ0 + (idx - chunkIndex) * currentΔj;
|
||||
if (jValues[idx] !== expectedJ || idx - chunkIndex >= 65536) {
|
||||
chunks.push({ records: currentChunk, j0: currentJ0, Δj: currentΔj });
|
||||
currentChunk = [records[idx]];
|
||||
currentJ0 = jValues[idx];
|
||||
currentΔj = idx + 1 < records.length ? jValues[idx + 1] - jValues[idx] : 0;
|
||||
} else {
|
||||
currentChunk.push(records[idx]);
|
||||
}
|
||||
}
|
||||
if (currentChunk.length > 0) {
|
||||
chunks.push({ records: currentChunk, j0: currentJ0, Δj: currentΔj });
|
||||
}
|
||||
|
||||
// Calculate total size for all chunks in this group by simulating offsets
|
||||
const chunkSizes = chunks.map(({ records: chunkRecords }) => {
|
||||
if (chunkRecords.length > 65535) throw new Error(`Chunk size exceeds 65535 for i=${iValue}`);
|
||||
let simulatedOffset = 0; // Relative to chunk start
|
||||
simulatedOffset += 12; // Header
|
||||
simulatedOffset += Δelems.length + elems.length; // Preface
|
||||
simulatedOffset += Δelems.reduce((sum, e) => sum + typeToBytes[e.baseType.name], 0); // Initial values
|
||||
while (simulatedOffset % 4 !== 0) simulatedOffset++; // Pad after initial
|
||||
simulatedOffset += chunkRecords.length * (
|
||||
Δelems.reduce((sum, e) => sum + typeToBytes[e.incrType.name], 0) +
|
||||
elems.reduce((sum, e) => sum + typeToBytes[e.type.name], 0)
|
||||
); // Interleaved record data
|
||||
while (simulatedOffset % 4 !== 0) simulatedOffset++; // Pad after record
|
||||
return simulatedOffset;
|
||||
});
|
||||
const totalChunkSize = chunkSizes.reduce((sum, size) => sum + size, 0);
|
||||
|
||||
// Start a new bundle if needed
|
||||
const lastBundle = buffers[buffers.length - 1];
|
||||
if (!lastBundle || lastBundle.offset + totalChunkSize > maxBundleSize) {
|
||||
buffers.push({ offset: 4, buffer: null, view: null });
|
||||
}
|
||||
|
||||
// Initialize DataView for current bundle
|
||||
const currentBundle = buffers[buffers.length - 1];
|
||||
if (!currentBundle.buffer) {
|
||||
const requiredSize = totalChunkSize + 4;
|
||||
currentBundle.buffer = new ArrayBuffer(requiredSize);
|
||||
currentBundle.view = new DataView(currentBundle.buffer);
|
||||
}
|
||||
|
||||
// Process each chunk
|
||||
for (const { records: chunkRecords, j0, Δj } of chunks) {
|
||||
const chunkSize = chunkSizes.shift();
|
||||
|
||||
// Ensure buffer is large enough
|
||||
if (currentBundle.offset + chunkSize > currentBundle.buffer.byteLength) {
|
||||
const newSize = currentBundle.offset + chunkSize;
|
||||
const newBuffer = new ArrayBuffer(newSize);
|
||||
new Uint8Array(newBuffer).set(new Uint8Array(currentBundle.buffer));
|
||||
currentBundle.buffer = newBuffer;
|
||||
currentBundle.view = new DataView(newBuffer);
|
||||
}
|
||||
|
||||
// Write chunk header
|
||||
let offset = currentBundle.offset;
|
||||
currentBundle.view.setUint8(offset++, 0x12); // Chunk type
|
||||
currentBundle.view.setUint8(offset++, udv); // udv
|
||||
currentBundle.view.setUint16(offset, chunkRecords.length, true); offset += 2; // count
|
||||
currentBundle.view.setUint16(offset, iValue, true); offset += 2; // i
|
||||
currentBundle.view.setUint16(offset, j0, true); offset += 2; // j0
|
||||
currentBundle.view.setInt16(offset, Δj, true); offset += 2; // Δj
|
||||
currentBundle.view.setUint8(offset++, Δelems.length); // Δelem_count
|
||||
currentBundle.view.setUint8(offset++, elems.length); // elem_count
|
||||
|
||||
// Write chunk preface (element types)
|
||||
for (const elem of Δelems) {
|
||||
const baseCode = typeToCode[elem.baseType.name];
|
||||
const incrCode = typeToCode[elem.incrType.name];
|
||||
currentBundle.view.setUint8(offset++, (incrCode << 4) | baseCode);
|
||||
}
|
||||
for (const elem of elems) {
|
||||
currentBundle.view.setUint8(offset++, typeToCode[elem.type.name]);
|
||||
}
|
||||
|
||||
// Write initial values for Δelems
|
||||
for (const elem of Δelems) {
|
||||
const value = elem.key(chunkRecords[0]);
|
||||
if (value == null) throw new Error('Missing Δelem value from getter');
|
||||
writeTypedValue(currentBundle.view, offset, value, elem.baseType);
|
||||
offset += typeToBytes[elem.baseType.name];
|
||||
}
|
||||
// Pad to 4-byte boundary
|
||||
while (offset % 4 !== 0) currentBundle.view.setUint8(offset++, 0);
|
||||
|
||||
// Write interleaved record data
|
||||
const prevValues = Δelems.map(elem => elem.key(chunkRecords[0]));
|
||||
for (let idx = 0; idx < chunkRecords.length; idx++) {
|
||||
// Write Δelems increments
|
||||
for (let i = 0; i < Δelems.length; i++) {
|
||||
const elem = Δelems[i];
|
||||
const value = idx === 0 ? 0 : elem.key(chunkRecords[idx]) - prevValues[i];
|
||||
writeTypedValue(currentBundle.view, offset, value, elem.incrType);
|
||||
offset += typeToBytes[elem.incrType.name];
|
||||
prevValues[i] = elem.key(chunkRecords[idx]);
|
||||
}
|
||||
// Write elems
|
||||
for (const elem of elems) {
|
||||
const value = elem.key(chunkRecords[idx]);
|
||||
if (value == null) throw new Error('Missing elem value from getter');
|
||||
writeTypedValue(currentBundle.view, offset, value, elem.type);
|
||||
offset += typeToBytes[elem.type.name];
|
||||
}
|
||||
}
|
||||
// Pad to 4-byte boundary
|
||||
while (offset % 4 !== 0) currentBundle.view.setUint8(offset++, 0);
|
||||
|
||||
// Update bundle offset
|
||||
currentBundle.offset = offset;
|
||||
}
|
||||
|
||||
// Update bundle header
|
||||
currentBundle.view.setUint32(0, 0x1C | ((currentBundle.offset - 4) << 8), true);
|
||||
}
|
||||
|
||||
// Combine buffers into final Uint8Array
|
||||
const finalLength = buffers.reduce((sum, b) => sum + b.offset, 0);
|
||||
const result = new Uint8Array(finalLength);
|
||||
let offset = 0;
|
||||
for (const { buffer, offset: bundleOffset } of buffers) {
|
||||
result.set(new Uint8Array(buffer, 0, bundleOffset), offset);
|
||||
offset += bundleOffset;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function writeTypedValue(view, offset, value, type) {
|
||||
switch (type) {
|
||||
case Int8Array: view.setInt8(offset, value); break;
|
||||
case Uint8Array: view.setUint8(offset, value); break;
|
||||
case Int16Array: view.setInt16(offset, value, true); break;
|
||||
case Uint16Array: view.setUint16(offset, value, true); break;
|
||||
case Int32Array: view.setInt32(offset, value, true); break;
|
||||
case Uint32Array: view.setUint32(offset, value, true); break;
|
||||
case Float32Array: view.setFloat32(offset, value, true); break;
|
||||
case Float64Array: view.setFloat64(offset, value, true); break;
|
||||
case BigInt64Array: view.setBigInt64(offset, BigInt(value), true); break;
|
||||
case BigUint64Array: view.setBigUint64(offset, BigInt(value), true); break;
|
||||
default: throw new Error(`Unsupported type: ${type.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { sequential, interleaved };
|
||||
139
lib/modules/@dougal/binary/index.js
Normal file
139
lib/modules/@dougal/binary/index.js
Normal file
@@ -0,0 +1,139 @@
|
||||
|
||||
/** Binary encoder
|
||||
*
|
||||
* This module encodes scalar data from a grid-like source
|
||||
* into a packed binary format for bandwidth efficiency and
|
||||
* speed of access.
|
||||
*
|
||||
* Data are indexed by i & j values, with "i" being constant
|
||||
* (e.g., a sequence or line number) and "j" expected to change
|
||||
* by a constant, linear amount (e.g., point numbers). All data
|
||||
* from consecutive "j" values will be encoded as a single array
|
||||
* (or series of arrays if multiple values are encoded).
|
||||
* If there is a jump in the "j" progression, a new "chunk" will
|
||||
* be started with a new array (or series of arrays).
|
||||
*
|
||||
* Multiple values may be encoded per (i, j) pair, using any of
|
||||
* the types supported by JavaScript's TypedArray except for
|
||||
* Float16 and Uint8Clamped. Each variable can be encoded with
|
||||
* a different size.
|
||||
*
|
||||
* Values may be encoded directly or as deltas from an initial
|
||||
* value. The latter is particularly efficient when dealing with
|
||||
* monotonically incrementing data, such as timestamps.
|
||||
*
|
||||
* The conceptual packet format for sequentially encoded data
|
||||
* looks like this:
|
||||
*
|
||||
* <msg-type> <count: x> <i> <j0> <Δj>
|
||||
*
|
||||
* <Δelement_count: y>
|
||||
* <element_count: z>
|
||||
*
|
||||
* <Δelement_1_type_base> … <Δelement_y_type_base>
|
||||
* <Δelement_1_type_incr> … <Δelement_y_type_incr>
|
||||
* <elem_1_type> … <elem_z_type>
|
||||
*
|
||||
* <Δelement_1_first> … <Δelement_z_first>
|
||||
*
|
||||
* <Δelem_1_0> … <Δelem_1_x>
|
||||
* …
|
||||
* <Δelem_y_0> … <Δelem_y_x>
|
||||
* <elem_1_0> … <elem_1_x>
|
||||
* …
|
||||
* <elem_z_0> … <elem_z_x>
|
||||
*
|
||||
*
|
||||
* The conceptual packet format for interleaved encoded data
|
||||
* looks like this:
|
||||
*
|
||||
*
|
||||
* <msg-type> <count: x> <i> <j0> <Δj>
|
||||
*
|
||||
* <Δelement_count: y>
|
||||
* <element_count: z>
|
||||
*
|
||||
* <Δelement_1_type_base> … <Δelement_y_type_base>
|
||||
* <Δelement_1_type_incr> … <Δelement_y_type_incr>
|
||||
* <elem_1_type> … <elem_z_type>
|
||||
*
|
||||
* <Δelement_1_first> … <Δelement_y_first>
|
||||
*
|
||||
* <Δelem_1_0> <Δelem_2_0> … <Δelem_y_0> <elem_1_0> <elem_2_0> … <elem_z_0>
|
||||
* <Δelem_1_1> <Δelem_2_1> … <Δelem_y_1> <elem_1_1> <elem_2_1> … <elem_z_1>
|
||||
* …
|
||||
* <Δelem_1_x> <Δelem_2_x> … <Δelem_y_x> <elem_1_x> <elem_2_x> … <elem_z_x>
|
||||
*
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* json = [
|
||||
* {
|
||||
* sequence: 7,
|
||||
* sailline: 5354,
|
||||
* line: 5356,
|
||||
* point: 1068,
|
||||
* tstamp: 1695448704372,
|
||||
* objrefraw: 3,
|
||||
* objreffinal: 4
|
||||
* },
|
||||
* {
|
||||
* sequence: 7,
|
||||
* sailline: 5354,
|
||||
* line: 5352,
|
||||
* point: 1070,
|
||||
* tstamp: 1695448693612,
|
||||
* objrefraw: 2,
|
||||
* objreffinal: 3
|
||||
* },
|
||||
* {
|
||||
* sequence: 7,
|
||||
* sailline: 5354,
|
||||
* line: 5356,
|
||||
* point: 1072,
|
||||
* tstamp: 1695448684624,
|
||||
* objrefraw: 3,
|
||||
* objreffinal: 4
|
||||
* }
|
||||
* ];
|
||||
*
|
||||
* deltas = [
|
||||
* { key: el => el.tstamp, baseType: BigUint64Array, incrType: Int16Array }
|
||||
* ];
|
||||
*
|
||||
* elems = [
|
||||
* { key: el => el.objrefraw, type: Uint8Array },
|
||||
* { key: el => el.objreffinal, type: Uint8Array }
|
||||
* ];
|
||||
*
|
||||
* i = el => el.sequence;
|
||||
*
|
||||
* j = el => el.point;
|
||||
*
|
||||
* bundle = encode(json, i, j, deltas, elems);
|
||||
*
|
||||
* // bundle:
|
||||
*
|
||||
* Uint8Array(40) [
|
||||
* 36, 0, 0, 28, 17, 0, 3, 0, 7, 0,
|
||||
* 44, 4, 2, 0, 1, 2, 42, 1, 1, 116,
|
||||
* 37, 158, 192, 138, 1, 0, 0, 0, 0, 0,
|
||||
* 248, 213, 228, 220, 3, 2, 3, 4, 3, 4
|
||||
* ]
|
||||
*
|
||||
* decode(bundle);
|
||||
*
|
||||
* {
|
||||
* i: 7,
|
||||
* j: [ 1068, 1070, 1072 ],
|
||||
* 'Δelems': [ [ 1695448704372, 1695448693612, 1695448684624 ] ],
|
||||
* elems: [ [ 3, 2, 3 ], [ 4, 3, 4 ] ]
|
||||
* }
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
encode: {...require('./encode')},
|
||||
decode: {...require('./decode')},
|
||||
...require('./classes')
|
||||
};
|
||||
12
lib/modules/@dougal/binary/package.json
Normal file
12
lib/modules/@dougal/binary/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@dougal/binary",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": ""
|
||||
}
|
||||
25
lib/modules/@dougal/concurrency/index.js
Normal file
25
lib/modules/@dougal/concurrency/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
class ConcurrencyLimiter {
|
||||
|
||||
constructor(maxConcurrent) {
|
||||
this.maxConcurrent = maxConcurrent;
|
||||
this.active = 0;
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
async enqueue(task) {
|
||||
if (this.active >= this.maxConcurrent) {
|
||||
await new Promise(resolve => this.queue.push(resolve));
|
||||
}
|
||||
this.active++;
|
||||
try {
|
||||
return await task();
|
||||
} finally {
|
||||
this.active--;
|
||||
if (this.queue.length > 0) {
|
||||
this.queue.shift()();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConcurrencyLimiter;
|
||||
12
lib/modules/@dougal/concurrency/package.json
Normal file
12
lib/modules/@dougal/concurrency/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@dougal/concurrency",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": ""
|
||||
}
|
||||
24
lib/modules/@dougal/user/package-lock.json
generated
24
lib/modules/@dougal/user/package-lock.json
generated
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
19794
lib/www/client/source/package-lock.json
generated
19794
lib/www/client/source/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,14 @@
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deck.gl/aggregation-layers": "^9.1.13",
|
||||
"@deck.gl/geo-layers": "^9.1.13",
|
||||
"@deck.gl/mesh-layers": "^9.1.14",
|
||||
"@dougal/binary": "file:../../../modules/@dougal/binary",
|
||||
"@dougal/concurrency": "file:../../../modules/@dougal/concurrency",
|
||||
"@dougal/organisations": "file:../../../modules/@dougal/organisations",
|
||||
"@dougal/user": "file:../../../modules/@dougal/user",
|
||||
"@loaders.gl/obj": "^4.3.4",
|
||||
"@mdi/font": "^7.2.96",
|
||||
"buffer": "^6.0.3",
|
||||
"core-js": "^3.6.5",
|
||||
@@ -19,6 +25,7 @@
|
||||
"leaflet-arrowheads": "^1.2.2",
|
||||
"leaflet-realtime": "^2.2.0",
|
||||
"leaflet.markercluster": "^1.4.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"marked": "^9.1.4",
|
||||
"path-browserify": "^1.0.1",
|
||||
"plotly.js-dist": "^2.27.0",
|
||||
|
||||
406982
lib/www/client/source/public/assets/boat0.obj
Normal file
406982
lib/www/client/source/public/assets/boat0.obj
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@
|
||||
:color="snackColour"
|
||||
:timeout="6000"
|
||||
>
|
||||
{{ snackText }}
|
||||
<div v-html="snackText"></div>
|
||||
<template v-slot:action="{ attrs }">
|
||||
<v-btn
|
||||
text
|
||||
@@ -52,9 +52,8 @@ export default {
|
||||
}),
|
||||
|
||||
computed: {
|
||||
snackText () { return this.$store.state.snack.snackText },
|
||||
snackText () { return this.$root.markdownInline(this.$store.state.snack.snackText) },
|
||||
snackColour () { return this.$store.state.snack.snackColour },
|
||||
...mapGetters(["serverEvent"])
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -77,24 +76,39 @@ export default {
|
||||
this.$store.commit('setSnackText', "");
|
||||
}
|
||||
},
|
||||
|
||||
async serverEvent (event) {
|
||||
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});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
handleJWT (context, {payload}) {
|
||||
this.setCredentials({token: payload.token});
|
||||
},
|
||||
|
||||
handleProject (context, {payload}) {
|
||||
this.refreshProjects();
|
||||
},
|
||||
|
||||
registerNotificationHandlers () {
|
||||
|
||||
this.$store.dispatch('registerHandler', {
|
||||
table: '.jwt',
|
||||
handler: this.handleJWT
|
||||
});
|
||||
|
||||
this.$store.dispatch('registerHandler', {
|
||||
table: 'project',
|
||||
handler: this.handleProject
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
...mapActions(["setCredentials", "refreshProjects"])
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
// Local Storage values are always strings
|
||||
this.$vuetify.theme.dark = localStorage.getItem("darkTheme") == "true";
|
||||
this.registerNotificationHandlers();
|
||||
await this.setCredentials();
|
||||
this.refreshProjects();
|
||||
}
|
||||
|
||||
@@ -9,8 +9,17 @@
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-icon v-if="serverConnected" class="mr-6" small title="Connected to server">mdi-lan-connect</v-icon>
|
||||
<v-icon v-else class="mr-6" small color="red" title="Server connection lost (we'll reconnect automatically when the server comes back)">mdi-lan-disconnect</v-icon>
|
||||
<template v-if="isFrontendRemote">
|
||||
<template v-if="serverConnected">
|
||||
<v-icon v-if="isGatewayReliable" class="mr-6" title="Connected to server via gateway">mdi-cloud-outline</v-icon>
|
||||
<v-icon v-else class="mr-6" color="orange" title="Gateway connection is unreliable. Expect outages.">mdi-cloud-off</v-icon>
|
||||
</template>
|
||||
<v-icon v-else class="mr-6" color="red" :title="`Server connection lost: the gateway cannot reach the remote server.\nWe will reconnect automatically when the link with the remote server is restored.`">mdi-cloud-off</v-icon>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-icon v-if="serverConnected" class="mr-6" small title="Connected to server">mdi-lan-connect</v-icon>
|
||||
<v-icon v-else class="mr-6" small color="red" :title="`Server connection lost.\nWe will reconnect automatically when the server comes back.`">mdi-lan-disconnect</v-icon>
|
||||
</template>
|
||||
|
||||
<dougal-notifications-control class="mr-6"></dougal-notifications-control>
|
||||
|
||||
@@ -51,13 +60,39 @@ export default {
|
||||
DougalNotificationsControl
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
lastGatewayErrorTimestamp: 0,
|
||||
gatewayErrorSilencePeriod: 60000,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
year () {
|
||||
const date = new Date();
|
||||
return date.getUTCFullYear();
|
||||
},
|
||||
|
||||
...mapState({serverConnected: state => state.notify.serverConnected})
|
||||
...mapState({
|
||||
serverConnected: state => state.notify.serverConnected,
|
||||
isFrontendRemote: state => state.api.serverInfo?.["remote-frontend"] ?? false,
|
||||
isGatewayReliable: state => state.api.isGatewayReliable
|
||||
})
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
isGatewayReliable (val) {
|
||||
if (val === false) {
|
||||
const elapsed = Date.now() - this.lastGatewayErrorTimestamp;
|
||||
const lastGatewayErrorTimestamp = Date.now();
|
||||
if (elapsed > this.gatewayErrorSilencePeriod) {
|
||||
this.$root.showSnack("Gateway error", "warning");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<v-card-title class="headline">
|
||||
Array inline / crossline error
|
||||
<v-spacer></v-spacer>
|
||||
<!--
|
||||
<v-switch v-model="scatterplot" label="Scatterplot"></v-switch>
|
||||
<v-switch class="ml-4" v-model="histogram" label="Histogram"></v-switch>
|
||||
-->
|
||||
</v-card-title>
|
||||
|
||||
<v-container fluid fill-height>
|
||||
@@ -57,8 +59,8 @@ export default {
|
||||
graph: [],
|
||||
busy: false,
|
||||
resizeObserver: null,
|
||||
scatterplot: false,
|
||||
histogram: false
|
||||
scatterplot: true,
|
||||
histogram: true
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<v-card-title class="headline">
|
||||
Gun depth
|
||||
<v-spacer></v-spacer>
|
||||
<!--
|
||||
<v-switch v-model="shotpoint" label="Shotpoint"></v-switch>
|
||||
<v-switch class="ml-4" v-model="violinplot" label="Violin plot"></v-switch>
|
||||
-->
|
||||
</v-card-title>
|
||||
|
||||
<v-container fluid fill-height>
|
||||
@@ -59,7 +61,7 @@ export default {
|
||||
busy: false,
|
||||
resizeObserver: null,
|
||||
shotpoint: true,
|
||||
violinplot: false
|
||||
violinplot: true
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<v-card-title class="headline">
|
||||
Gun pressures
|
||||
<v-spacer></v-spacer>
|
||||
<!--
|
||||
<v-switch v-model="shotpoint" label="Shotpoint"></v-switch>
|
||||
<v-switch class="ml-4" v-model="violinplot" label="Violin plot"></v-switch>
|
||||
-->
|
||||
</v-card-title>
|
||||
|
||||
<v-container fluid fill-height>
|
||||
@@ -59,7 +61,7 @@ export default {
|
||||
busy: false,
|
||||
resizeObserver: null,
|
||||
shotpoint: true,
|
||||
violinplot: false
|
||||
violinplot: true
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<v-card-title class="headline">
|
||||
Gun timing
|
||||
<v-spacer></v-spacer>
|
||||
<!--
|
||||
<v-switch v-model="shotpoint" label="Shotpoint"></v-switch>
|
||||
<v-switch class="ml-4" v-model="violinplot" label="Violin plot"></v-switch>
|
||||
-->
|
||||
</v-card-title>
|
||||
|
||||
<v-container fluid fill-height>
|
||||
@@ -59,7 +61,7 @@ export default {
|
||||
busy: false,
|
||||
resizeObserver: null,
|
||||
shotpoint: true,
|
||||
violinplot: false
|
||||
violinplot: true
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -14,15 +14,51 @@
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
Dougal user support
|
||||
</v-card-title>
|
||||
<v-window v-model="page">
|
||||
<v-window-item value="support">
|
||||
<v-card-title class="headline">
|
||||
Dougal user support
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<p>You can get help or report a problem by sending an email to <a :href="`mailto:${email}`">{{email}}</a>. Please include as much information as possible about your problem or question—screenshots are often a good idea, and data files may also be attached.</p>
|
||||
<v-card-text>
|
||||
<p>You can get help or report a problem by sending an email to <a :href="`mailto:${email}`">{{email}}</a>. Please include as much information as possible about your problem or question—screenshots are often a good idea, and data files may also be attached.</p>
|
||||
|
||||
<p>When you write to the above address a ticket will be automatically created in the project's issue tracking system.</p>
|
||||
</v-card-text>
|
||||
<p>When you write to the above address a ticket will be automatically created in the project's issue tracking system.</p>
|
||||
|
||||
<v-alert dense type="info" border="left" outlined>
|
||||
<div class="text-body-2">
|
||||
You are using Dougal version:
|
||||
<ul>
|
||||
<li><code>{{clientVersion}}</code> (client)</li>
|
||||
<li><code>{{serverVersion}}</code> (server)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
</v-card-text>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="changelog">
|
||||
<v-card-title class="headline">
|
||||
Dougal release notes
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-carousel v-model="releaseShown"
|
||||
:continuous="false"
|
||||
:cycle="false"
|
||||
:show-arrows="true"
|
||||
:hide-delimiters="true"
|
||||
>
|
||||
<v-carousel-item v-for="release in releaseHistory">
|
||||
<pre>{{release}}</pre>
|
||||
</v-carousel-item>
|
||||
</v-carousel>
|
||||
</v-card-text>
|
||||
|
||||
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
@@ -46,6 +82,7 @@
|
||||
<span class="d-none d-lg-inline">Report a bug</span>
|
||||
</v-btn>
|
||||
|
||||
<!---
|
||||
<v-btn
|
||||
color="info"
|
||||
text
|
||||
@@ -54,6 +91,17 @@
|
||||
>
|
||||
<v-icon>mdi-rss</v-icon>
|
||||
</v-btn>
|
||||
--->
|
||||
|
||||
<v-btn v-if="versionHistory"
|
||||
color="info"
|
||||
text
|
||||
:title="page == 'support' ? 'View release notes' : 'View support info'"
|
||||
:input-value="page == 'changelog'"
|
||||
@click="page = page == 'support' ? 'changelog' : 'support'"
|
||||
>
|
||||
<v-icon>mdi-history</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
@@ -75,6 +123,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'DougalHelpDialog',
|
||||
|
||||
@@ -82,8 +132,38 @@ export default {
|
||||
return {
|
||||
dialog: false,
|
||||
email: "dougal-support@aaltronav.eu",
|
||||
feed: btoa(encodeURIComponent("https://gitlab.com/wgp/dougal/software.atom?feed_token=XSPpvsYEny8YmH75Nz5W"))
|
||||
feed: btoa(encodeURIComponent("https://gitlab.com/wgp/dougal/software.atom?feed_token=XSPpvsYEny8YmH75Nz5W")),
|
||||
clientVersion: process.env.DOUGAL_FRONTEND_VERSION ?? "(unknown)",
|
||||
serverVersion: null,
|
||||
versionHistory: null,
|
||||
releaseHistory: [],
|
||||
releaseShown: null,
|
||||
page: "support"
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getServerVersion () {
|
||||
if (!this.serverVersion) {
|
||||
const version = await this.api(['/version', {}, null, {silent:true}]);
|
||||
this.serverVersion = version?.tag ?? "(unknown)";
|
||||
}
|
||||
if (!this.versionHistory) {
|
||||
const history = await this.api(['/version/history?count=3', {}, null, {silent:true}]);
|
||||
this.releaseHistory = history;
|
||||
this.versionHistory = history?.[this.serverVersion.replace(/-.*$/, "")] ?? null;
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
this.getServerVersion();
|
||||
},
|
||||
|
||||
async beforeUpdate () {
|
||||
this.getServerVersion();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<div class="line-status" v-if="sequences.length == 0">
|
||||
<slot name="empty"></slot>
|
||||
</div>
|
||||
<div class="line-status" v-else-if="sequenceHref || plannedSequenceHref || pendingReshootHref">
|
||||
<div class="line-status" v-if="sequenceHref || plannedSequenceHref || pendingReshootHref">
|
||||
<router-link v-for="sequence in sequences" :key="sequence.sequence" v-if="sequenceHref"
|
||||
class="sequence"
|
||||
:class="sequence.status"
|
||||
@@ -26,7 +23,7 @@
|
||||
>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="line-status" v-else>
|
||||
<div class="line-status" v-else-if="sequences.length || plannedSequences.length || Object.keys(pendingReshoots).length">
|
||||
<div v-for="sequence in sequences" :key="sequence.sequence"
|
||||
class="sequence"
|
||||
:class="sequence.status"
|
||||
@@ -47,6 +44,9 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line-status" v-else>
|
||||
<slot name="empty"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
<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-tab tab-value="paths">Paths</v-tab>
|
||||
<v-tab tab-value="globs">Globs</v-tab>
|
||||
<v-tab tab-value="pattern" v-if="pattern">Pattern</v-tab>
|
||||
<v-tab tab-value="lineNameInfo" v-if="lineNameInfo">Line info</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-items v-model="tab">
|
||||
|
||||
<v-tab-item>
|
||||
<v-tab-item value="paths">
|
||||
<v-card flat>
|
||||
<v-card-subtitle>
|
||||
A list of directories which are searched for matching files.
|
||||
@@ -56,7 +56,7 @@
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item>
|
||||
<v-tab-item value="globs">
|
||||
<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.
|
||||
@@ -93,7 +93,7 @@
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item v-if="pattern">
|
||||
<v-tab-item value="pattern" 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.
|
||||
@@ -153,7 +153,7 @@
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab-item v-if="lineNameInfo">
|
||||
<v-tab-item value="lineNameInfo">
|
||||
<v-card flat>
|
||||
<v-card-subtitle>
|
||||
Line information that will be extracted from file names
|
||||
@@ -165,14 +165,14 @@
|
||||
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-model="lineNameInfo_.example"
|
||||
></v-text-field>
|
||||
|
||||
<dougal-fixed-string-decoder
|
||||
:multiline="true"
|
||||
:text="lineNameInfo.example"
|
||||
:fixed.sync="lineNameInfo.fixed"
|
||||
:fields.sync="lineNameInfo.fields"
|
||||
:text="lineNameInfo_.example"
|
||||
:fixed.sync="lineNameInfo_.fixed"
|
||||
:fields.sync="lineNameInfo_.fields"
|
||||
></dougal-fixed-string-decoder>
|
||||
|
||||
</v-form>
|
||||
@@ -195,6 +195,23 @@
|
||||
@click="reset"
|
||||
>Reset</v-btn>
|
||||
-->
|
||||
<v-btn
|
||||
v-if="tab=='lineNameInfo'"
|
||||
:disabled="!validLineNameInfo"
|
||||
@click="copyLineNameInfo"
|
||||
title="Copy this definition into the clipboard. It can then be pasted into other sections or configurations."
|
||||
>
|
||||
<v-icon left>mdi-content-copy</v-icon>
|
||||
Copy
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="tab=='lineNameInfo'"
|
||||
@click="pasteLineNameInfo"
|
||||
title="Paste a line info definition copied from elsewhere"
|
||||
>
|
||||
<v-icon left>mdi-content-paste</v-icon>
|
||||
Paste
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@@ -253,6 +270,9 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
validLineNameInfo () {
|
||||
return typeof this.lineNameInfo == 'object';
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -285,6 +305,28 @@ export default {
|
||||
|
||||
methods: {
|
||||
|
||||
async copyLineNameInfo () {
|
||||
await navigator.clipboard.writeText(JSON.stringify(this.lineNameInfo, null, 4));
|
||||
this.showSnack(["Line name information copied to clipboard", "primary"]);
|
||||
},
|
||||
|
||||
async pasteLineNameInfo () {
|
||||
const text = await navigator.clipboard.readText();
|
||||
try {
|
||||
const data = JSON.parse(text);
|
||||
if (["fixed", "fields", "example"].every( key => key in data )) {
|
||||
this.$emit("update:lineNameInfo", data);
|
||||
this.showSnack(["Line name information pasted from clipboard", "primary"]);
|
||||
} else {
|
||||
this.showSnack(["Clipboard contents are not valid line name information", "error"]);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
this.showSnack(["Clipboard contents are not valid line name information", "error"]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.globs_ = this.globs;
|
||||
this.paths_ = this.paths;
|
||||
@@ -302,6 +344,8 @@ export default {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
...mapActions(["showSnack"])
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
|
||||
@@ -519,17 +519,18 @@ export default {
|
||||
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")) || "";
|
||||
const opts = {format: "text"};
|
||||
const head = await this.api([url, init, null, opts]);
|
||||
return typeof head === "string"
|
||||
? head?.substring(0, head.lastIndexOf("\n")) || ""
|
||||
: this.head ?? "";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
1
lib/www/client/source/src/lib/binary
Symbolic link
1
lib/www/client/source/src/lib/binary
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../server/lib/binary
|
||||
146
lib/www/client/source/src/lib/deck.gl/DougalBinaryLoader.js
Normal file
146
lib/www/client/source/src/lib/deck.gl/DougalBinaryLoader.js
Normal file
@@ -0,0 +1,146 @@
|
||||
// src/lib/deck.gl/DougalBinaryLoader.js
|
||||
import { LoaderObject } from '@loaders.gl/core';
|
||||
import { DougalBinaryBundle } from '@dougal/binary';
|
||||
|
||||
async function cachedFetch(url, init, opts = {}) {
|
||||
let res; // The response
|
||||
let cache; // Potentially, a Cache API cache name
|
||||
let isCached;
|
||||
|
||||
if (opts?.cache === true) {
|
||||
opts.cache = { name: "dougal" };
|
||||
} else if (typeof opts?.cache === "string") {
|
||||
opts.cache = { name: opts.cache };
|
||||
} else if (opts?.cache) {
|
||||
if (!(opts.cache instanceof Object)) {
|
||||
opts.cache = { name: "dougal" }
|
||||
} else if (!(opts.cache.name)) {
|
||||
opts.cache.name = "dougal";
|
||||
}
|
||||
}
|
||||
|
||||
if (opts?.cache && window.cache) {
|
||||
cache = await caches.open(opts.cache.name);
|
||||
res = await cache.match(url);
|
||||
isCached = !!res;
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
res = await fetch(url, init);
|
||||
}
|
||||
|
||||
if (cache && !isCached) {
|
||||
cache.put(url, res.clone());
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
const DougalBinaryLoader = {
|
||||
name: 'DougalBinaryBundle Loader',
|
||||
extensions: ['dbb'],
|
||||
mimeTypes: ['application/vnd.aaltronav.dougal+octet-stream'],
|
||||
parse: async (input, options) => {
|
||||
let arrayBuffer;
|
||||
if (typeof input === 'string') {
|
||||
// Input is URL, fetch with caching
|
||||
const response = await cachedFetch(input, options?.fetch, options);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch: ${response.statusText}`);
|
||||
}
|
||||
arrayBuffer = await response.arrayBuffer();
|
||||
} else if (input instanceof ArrayBuffer) {
|
||||
arrayBuffer = input;
|
||||
} else {
|
||||
throw new Error('Invalid input: Expected URL string or ArrayBuffer');
|
||||
}
|
||||
|
||||
const bundle = DougalBinaryBundle.clone(arrayBuffer);
|
||||
|
||||
// Calculate total points
|
||||
const totalCount = bundle.chunks().reduce((acc, chunk) => acc + chunk.jCount, 0);
|
||||
|
||||
// Prepare positions (Float32Array: [lon1, lat1, lon2, lat2, ...])
|
||||
const positions = new Float32Array(totalCount * 2);
|
||||
|
||||
// Extract udv (assume constant across chunks)
|
||||
const udv = bundle.chunks()[0].udv;
|
||||
|
||||
// Prepare values as an array of TypedArrays
|
||||
const ΔelemCount = bundle.chunks()[0].ΔelemCount;
|
||||
const elemCount = bundle.chunks()[0].elemCount;
|
||||
const values = new Array(ΔelemCount + elemCount + 2);
|
||||
|
||||
// Initialize values arrays with correct types
|
||||
if (udv == 0) {
|
||||
for (let k = 0; k < values.length; k++) {
|
||||
values[k] = new (k === 0 ? Uint16Array : k === 1 ? Uint32Array : Uint8Array)(totalCount);
|
||||
}
|
||||
} else if (udv == 1) {
|
||||
for (let k = 0; k < values.length; k++) {
|
||||
values[k] = new (k === 0 ? Uint16Array : k === 1 ? Uint32Array : k === 2 ? Uint8Array : Uint16Array)(totalCount);
|
||||
}
|
||||
} else if (udv == 2) {
|
||||
for (let k = 0; k < values.length; k++) {
|
||||
values[k] = new (k === 0 ? Uint16Array : k === 1 ? Uint32Array : k === 2 ? BigUint64Array : Float32Array)(totalCount);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Invalid udv: Expected 0, 1, or 2; found ${udv}`);
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
for (const chunk of bundle.chunks()) {
|
||||
const λarray = chunk.elem(0);
|
||||
const φarray = chunk.elem(1);
|
||||
for (let i = 0; i < λarray.length; i++) {
|
||||
positions[offset * 2 + i * 2] = λarray[i];
|
||||
positions[offset * 2 + i * 2 + 1] = φarray[i];
|
||||
}
|
||||
|
||||
values[0].set(new Uint16Array(chunk.jCount).fill(chunk.i), offset);
|
||||
values[1].set(Uint32Array.from({ length: chunk.jCount }, (_, i) => chunk.j0 + i * chunk.Δj), offset);
|
||||
|
||||
for (let j = 0; j < ΔelemCount; j++) {
|
||||
values[2 + j].set(chunk.Δelem(j), offset);
|
||||
}
|
||||
for (let j = 2; j < elemCount; j++) {
|
||||
values[2 + ΔelemCount + j - 2].set(chunk.elem(j), offset);
|
||||
}
|
||||
|
||||
offset += chunk.jCount;
|
||||
}
|
||||
|
||||
console.log(`Parsed ${totalCount} points, ${values.length} value arrays`);
|
||||
|
||||
const attributes = {
|
||||
getPosition: {
|
||||
value: positions,
|
||||
type: 'float32',
|
||||
size: 2
|
||||
},
|
||||
udv
|
||||
};
|
||||
|
||||
values.forEach((valArray, k) => {
|
||||
let value = valArray;
|
||||
if (valArray instanceof BigUint64Array) {
|
||||
value = Float64Array.from(valArray, v => Number(v));
|
||||
}
|
||||
attributes[`value${k}`] = {
|
||||
value,
|
||||
type: value instanceof Float64Array ? 'float64' :
|
||||
value instanceof Uint16Array ? 'uint16' :
|
||||
value instanceof Uint32Array ? 'uint32' : 'float32',
|
||||
size: 1
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
length: totalCount,
|
||||
attributes
|
||||
};
|
||||
},
|
||||
options: {} // Optional: Add custom options if needed
|
||||
};
|
||||
|
||||
export default DougalBinaryLoader;
|
||||
144
lib/www/client/source/src/lib/deck.gl/DougalEventsLayer.js
Normal file
144
lib/www/client/source/src/lib/deck.gl/DougalEventsLayer.js
Normal file
@@ -0,0 +1,144 @@
|
||||
// Ref.: https://deck.gl/docs/developer-guide/custom-layers/composite-layers
|
||||
import { CompositeLayer } from '@deck.gl/core';
|
||||
import { GeoJsonLayer, ColumnLayer } from '@deck.gl/layers';
|
||||
|
||||
class DougalEventsLayer extends CompositeLayer {
|
||||
static layerName = "DougalEventsLayer";
|
||||
|
||||
static defaultProps = {
|
||||
columnsZoom: 11, // Threshold zoom level for switching layers
|
||||
jitter: 0, // Add a small amount of jitter so that columns do not overlap.
|
||||
// GeoJsonLayer props
|
||||
getLineColor: [127, 65, 90],
|
||||
getFillColor: [127, 65, 90],
|
||||
getPointRadius: 2,
|
||||
radiusUnits: "pixels",
|
||||
pointRadiusMinPixels: 2,
|
||||
lineWidthMinPixels: 2,
|
||||
// ColumnLayer props
|
||||
getPosition: { type: 'accessor', value: d => d.geometry.coordinates },
|
||||
getElevation: { type: 'accessor', value: d => Math.min(Math.max(d.properties.remarks?.length || 10, 10), 200) },
|
||||
diskResolution: 20,
|
||||
radius: 5,
|
||||
radiusUnits: "pixels",
|
||||
radiusScale: 1,
|
||||
elevationScale: 1,
|
||||
filled: true,
|
||||
stroked: false,
|
||||
extruded: true,
|
||||
wireframe: false,
|
||||
material: true,
|
||||
getFillColor: [255, 0, 0, 200],
|
||||
getLineColor: [255, 0, 0, 200],
|
||||
getLineWidth: 2,
|
||||
pickable: true
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.uid = "el-" + Math.random().toString().slice(2);
|
||||
// Initialize state with current zoom
|
||||
this.state = {
|
||||
zoom: this.context?.viewport?.zoom || 0
|
||||
};
|
||||
}
|
||||
|
||||
shouldUpdateState({ changeFlags }) {
|
||||
// Always update if viewport changed (including zoom)
|
||||
if (changeFlags.viewportChanged) {
|
||||
return true;
|
||||
}
|
||||
return super.shouldUpdateState({ changeFlags });
|
||||
}
|
||||
|
||||
updateState({ props, oldProps, context, changeFlags }) {
|
||||
// Check if zoom has changed
|
||||
const newZoom = context.viewport?.zoom || 0;
|
||||
if (newZoom !== this.state.zoom) {
|
||||
this.setState({ zoom: newZoom });
|
||||
this.setNeedsRedraw(); // Trigger re-render of sublayers
|
||||
console.log(`Zoom changed to ${newZoom}, triggering redraw`);
|
||||
}
|
||||
}
|
||||
|
||||
getPickingInfo({ info, mode, sourceLayer }) {
|
||||
if (info.index >= 0) {
|
||||
info.object = {
|
||||
...info.object // Merge default picking info (GeoJSON feature or ColumnLayer object)
|
||||
};
|
||||
if (sourceLayer) {
|
||||
info.object.type = sourceLayer.constructor.layerName;
|
||||
}
|
||||
//console.log(`Picked ${info.object.type}, index ${info.index}`);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
renderLayers() {
|
||||
const { zoom } = this.state;
|
||||
const sublayers = [];
|
||||
|
||||
if (zoom >= this.props.columnsZoom) {
|
||||
// Render ColumnLayer at high zoom
|
||||
const data = Array.isArray(this.props.data) ? this.props.data : this.props.data.features || [];
|
||||
|
||||
const positionFn = this.props.jitter
|
||||
? (d, info) => {
|
||||
let pos;
|
||||
if (typeof this.props.getPosition == 'function') {
|
||||
pos = this.props.getPosition(d, info);
|
||||
} else {
|
||||
pos = this.props.getPosition;
|
||||
}
|
||||
return pos.map( i => i + (Math.random() - 0.5) * this.props.jitter )
|
||||
}
|
||||
: this.props.getPosition;
|
||||
|
||||
sublayers.push(
|
||||
new ColumnLayer(this.getSubLayerProps({
|
||||
id: `${this.uid}-column`,
|
||||
data,
|
||||
visible: this.props.visible,
|
||||
getPosition: positionFn,
|
||||
getElevation: this.props.getElevation,
|
||||
diskResolution: this.props.diskResolution,
|
||||
radius: this.props.radius,
|
||||
radiusUnits: this.props.radiusUnits,
|
||||
radiusScale: this.props.radiusScale,
|
||||
elevationScale: this.props.elevationScale,
|
||||
filled: this.props.filled,
|
||||
stroked: this.props.stroked,
|
||||
extruded: this.props.extruded,
|
||||
wireframe: this.props.wireframe,
|
||||
material: this.props.material,
|
||||
getFillColor: this.props.getFillColor,
|
||||
getLineColor: this.props.getLineColor,
|
||||
getLineWidth: this.props.getLineWidth,
|
||||
pickable: this.props.pickable
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
// Render GeoJsonLayer at low zoom
|
||||
sublayers.push(
|
||||
new GeoJsonLayer(this.getSubLayerProps({
|
||||
id: `${this.uid}-geojson`,
|
||||
data: this.props.data,
|
||||
visible: this.props.visible,
|
||||
getLineColor: this.props.getLineColor,
|
||||
getFillColor: this.props.getFillColor,
|
||||
getPointRadius: this.props.getPointRadius,
|
||||
radiusUnits: this.props.radiusUnits,
|
||||
pointRadiusMinPixels: this.props.pointRadiusMinPixels,
|
||||
lineWidthMinPixels: this.props.lineWidthMinPixels,
|
||||
pickable: this.props.pickable
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Rendering ${sublayers.length} sublayer(s) at zoom ${zoom}`);
|
||||
|
||||
return sublayers;
|
||||
}
|
||||
}
|
||||
|
||||
export default DougalEventsLayer;
|
||||
108
lib/www/client/source/src/lib/deck.gl/DougalSequenceLayer.js
Normal file
108
lib/www/client/source/src/lib/deck.gl/DougalSequenceLayer.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// Ref.: https://deck.gl/docs/developer-guide/custom-layers/layer-lifecycle
|
||||
import { ScatterplotLayer } from '@deck.gl/layers';
|
||||
|
||||
class DougalSequenceLayer extends ScatterplotLayer {
|
||||
static layerName = "DougalSequenceLayer";
|
||||
|
||||
static defaultProps = {
|
||||
...ScatterplotLayer.defaultProps,
|
||||
valueIndex: 0,
|
||||
radiusUnits: "pixels",
|
||||
radiusScale: 1,
|
||||
lineWidthUnits: "pixels",
|
||||
lineWidthScale: 1,
|
||||
stroked: false,
|
||||
filled: true,
|
||||
radiusMinPixels: 1,
|
||||
radiusMaxPixels: 50,
|
||||
lineWidthMinPixels: 1,
|
||||
lineWidthMaxPixels: 50,
|
||||
getPosition: { type: 'accessor', value: d => d.positions },
|
||||
getRadius: 5,
|
||||
getFillColor: [255, 0, 0, 200],
|
||||
getLineColor: [255, 0, 0, 200],
|
||||
getLineWidth: 2,
|
||||
pickable: true
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
initializeState(context) {
|
||||
super.initializeState(context);
|
||||
}
|
||||
|
||||
getPickingInfo({ info, mode }) {
|
||||
const index = info.index;
|
||||
if (index >= 0) {
|
||||
const d = this.props.data.attributes;
|
||||
if (d) {
|
||||
if (d.udv == 0) {
|
||||
info.object = {
|
||||
udv: d.udv,
|
||||
i: d.value0.value[index],
|
||||
j: d.value1.value[index],
|
||||
ntba: d.value2.value[index] & 0x01,
|
||||
sailline_ntba: d.value2.value[index] & 0x02
|
||||
};
|
||||
} else if (d.udv == 1) {
|
||||
info.object = {
|
||||
udv: d.udv,
|
||||
i: d.value0.value[index],
|
||||
j: d.value1.value[index],
|
||||
sailline: d.value3.value[index],
|
||||
ntba: d.value2.value[index] & 0x01 ? true : false,
|
||||
sailline_ntba: d.value2.value[index] & 0x02 ? true : false
|
||||
};
|
||||
} else if (d.udv == 2) {
|
||||
info.object = {
|
||||
udv: d.udv,
|
||||
i: d.value0.value[index],
|
||||
j: d.value1.value[index],
|
||||
ts: Number(d.value2.value[index]),
|
||||
εi: d.value3.value[index] / 100,
|
||||
εj: d.value4.value[index] / 100,
|
||||
delta_μ: d.value5.value[index] / 10,
|
||||
delta_σ: d.value6.value[index] / 10,
|
||||
delta_R: d.value7.value[index] / 10,
|
||||
press_μ: d.value8.value[index],
|
||||
press_σ: d.value9.value[index],
|
||||
press_R: d.value10.value[index],
|
||||
depth_μ: d.value11.value[index] / 10,
|
||||
depth_σ: d.value12.value[index] / 10,
|
||||
depth_R: d.value13.value[index] / 10,
|
||||
fill_μ: d.value14.value[index],
|
||||
fill_σ: d.value15.value[index],
|
||||
fill_R: d.value16.value[index],
|
||||
delay_μ: d.value17.value[index] / 10,
|
||||
delay_σ: d.value18.value[index] / 10,
|
||||
delay_R: d.value19.value[index] / 10,
|
||||
nofire: d.value20.value[index] >> 4,
|
||||
autofire: d.value20.value[index] & 0xf
|
||||
};
|
||||
} else if (d.udv == 3) {
|
||||
info.object = {
|
||||
udv: d.udv,
|
||||
i: d.value0.value[index],
|
||||
j: d.value1.value[index],
|
||||
ts: Number(d.value2.value[index]),
|
||||
εi: d.value3.value[index] / 100,
|
||||
εj: d.value4.value[index] / 100,
|
||||
co_i: d.value5.value[index] / 100,
|
||||
co_j: d.value6.value[index] / 100,
|
||||
}
|
||||
} else {
|
||||
console.warn(`Unknown udv value ${d.udv}. No picking info`);
|
||||
info.object = {};
|
||||
}
|
||||
console.log(`Picked sequence ${info.object.i}, point ${info.object.j}, udv ${info.object.udv}`);
|
||||
} else {
|
||||
console.log(`No data found index = ${index}`);
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
export default DougalSequenceLayer;
|
||||
8
lib/www/client/source/src/lib/deck.gl/index.js
Normal file
8
lib/www/client/source/src/lib/deck.gl/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
import DougalSequenceLayer from './DougalSequenceLayer'
|
||||
import DougalEventsLayer from './DougalEventsLayer'
|
||||
|
||||
export {
|
||||
DougalSequenceLayer,
|
||||
DougalEventsLayer
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import router from './router'
|
||||
import store from './store'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import vueDebounce from 'vue-debounce'
|
||||
import { mapMutations } from 'vuex';
|
||||
import { mapMutations, mapActions } from 'vuex';
|
||||
import { markdown, markdownInline } from './lib/markdown';
|
||||
import { geometryAsString } from './lib/utils';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -46,6 +46,12 @@ new Vue({
|
||||
|
||||
methods: {
|
||||
|
||||
async sleep (ms = 0) {
|
||||
return await new Promise( (resolve) => {
|
||||
setTimeout( resolve, ms );
|
||||
});
|
||||
},
|
||||
|
||||
markdown (value) {
|
||||
return markdown(value);
|
||||
},
|
||||
@@ -56,12 +62,17 @@ new Vue({
|
||||
|
||||
showSnack(text, colour = "primary") {
|
||||
console.log("showSnack", text, colour);
|
||||
this.snackColour = colour;
|
||||
this.snackText = text;
|
||||
this.snack = true;
|
||||
this.$store.dispatch("showSnack", [text, colour]);
|
||||
},
|
||||
|
||||
sendJwt () {
|
||||
if (this.jwt) {
|
||||
this.ws.send(JSON.stringify({ jwt: this.jwt }));
|
||||
}
|
||||
},
|
||||
|
||||
initWs () {
|
||||
|
||||
if (this.ws) {
|
||||
console.log("WebSocket initWs already called");
|
||||
return;
|
||||
@@ -71,11 +82,12 @@ new Vue({
|
||||
|
||||
this.ws.addEventListener("message", (ev) => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
this.setServerEvent(msg);
|
||||
this.processServerEvent(msg);
|
||||
});
|
||||
|
||||
this.ws.addEventListener("open", (ev) => {
|
||||
console.log("WebSocket connection open", ev);
|
||||
this.sendJwt()
|
||||
this.setServerConnectionState(true);
|
||||
});
|
||||
|
||||
@@ -101,14 +113,13 @@ new Vue({
|
||||
}
|
||||
|
||||
this.wsCredentialsCheckTimer = setInterval( () => {
|
||||
this.ws.send(JSON.stringify({
|
||||
jwt: this.jwt
|
||||
}));
|
||||
this.sendJwt();
|
||||
}, this.wsCredentialsCheckInterval);
|
||||
|
||||
},
|
||||
|
||||
...mapMutations(['setServerEvent', 'setServerConnectionState'])
|
||||
...mapMutations(['setServerConnectionState']),
|
||||
...mapActions(['processServerEvent'])
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
const ConcurrencyLimiter = require('@dougal/concurrency');
|
||||
|
||||
/** Make an API request
|
||||
*
|
||||
* @a resource {String} is the target URL
|
||||
* @a init {Object} are the Fetch options
|
||||
* @a cb {Function} is a callback function: (res, err) => {}
|
||||
* @a opts {Object} are other optional parameters:
|
||||
* opts.silent {Boolean} controls whether snack messages are shown on failure
|
||||
* opts.cache {Object} controls whether Cache API is used
|
||||
* opts.cache.name {String} is the name of the cache to use. Defaults to "dougal"
|
||||
*
|
||||
* If Cache API is used, this function looks for a matching request in the cache
|
||||
* first, and returns it if found. If not found, it makes the request over the API
|
||||
* and then stores it in the cache.
|
||||
*
|
||||
* `opts.cache` may also be `true` (defaults to using the "dougal" cache),
|
||||
* a cache name (equivalent to {name: "…"}) or even an empty object (equivalent
|
||||
* to `true`).
|
||||
*/
|
||||
async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb, opts = {}]) {
|
||||
|
||||
const limiter = api.limiter || (api.limiter = new ConcurrencyLimiter(state.maxConcurrent));
|
||||
|
||||
try {
|
||||
commit("queueRequest");
|
||||
if (init && init.hasOwnProperty("body")) {
|
||||
@@ -15,22 +37,89 @@ async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb
|
||||
}
|
||||
// We also send Authorization: Bearer …
|
||||
if (getters.jwt) {
|
||||
init.credentials = "include";
|
||||
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);
|
||||
if (typeof cb === 'function') {
|
||||
await cb(null, res);
|
||||
|
||||
let res; // The response
|
||||
let cache; // Potentially, a Cache API cache name
|
||||
let isCached;
|
||||
|
||||
if (opts?.cache === true) {
|
||||
opts.cache = { name: "dougal" };
|
||||
} else if (typeof opts?.cache === "string") {
|
||||
opts.cache = { name: opts.cache };
|
||||
} else if (opts?.cache) {
|
||||
if (!(opts.cache instanceof Object)) {
|
||||
opts.cache = { name: "dougal" }
|
||||
} else if (!(opts.cache.name)) {
|
||||
opts.cache.name = "dougal";
|
||||
}
|
||||
}
|
||||
|
||||
if (opts?.cache && window.caches) {
|
||||
cache = await caches.open(opts.cache.name);
|
||||
res = await cache.match(url);
|
||||
isCached = !!res;
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
res = await limiter.enqueue(async () => await fetch(url, init));
|
||||
}
|
||||
|
||||
if (cache && !isCached && res.ok) { // Only cache successful responses
|
||||
cache.put(url, res.clone());
|
||||
}
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
await cb(null, res.clone());
|
||||
}
|
||||
|
||||
if (res.headers.has("x-dougal-server")) {
|
||||
const header = res.headers.get("x-dougal-server")
|
||||
const entries = header
|
||||
.split(";")
|
||||
.map(part => part.trim())
|
||||
.filter(part => part.length > 0)
|
||||
.map(part => {
|
||||
const idx = part.indexOf('=');
|
||||
if (idx === -1) {
|
||||
return [part, true];
|
||||
}
|
||||
const key = part.slice(0, idx).trim();
|
||||
const value = part.slice(idx + 1).trim();
|
||||
return [key, value];
|
||||
});
|
||||
state.serverInfo = entries.length ? Object.fromEntries(entries) : {};
|
||||
|
||||
if (state.serverInfo["remote-frontend"]) {
|
||||
state.isGatewayReliable = ![ 502, 503, 504 ].includes(res.status);
|
||||
} else {
|
||||
state.isGatewayReliable = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
|
||||
await dispatch('setCredentials');
|
||||
if (!isCached) {
|
||||
if (res.headers.has("x-jwt")) {
|
||||
await dispatch('setCredentials', { response: res });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return init.text ? (await res.text()) : (await res.json());
|
||||
if (!res.bodyUsed) { // It may have been consumed by a callback
|
||||
const validFormats = [ "arrayBuffer", "blob", "formData", "json", "text" ];
|
||||
if (opts.format && validFormats.includes(opts.format)) {
|
||||
return await res[opts.format]();
|
||||
} else {
|
||||
return await res.json();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
if (Number(res.headers.get("Content-Length")) === 0) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const state = () => ({
|
||||
apiUrl: "/api",
|
||||
requestsCount: 0
|
||||
requestsCount: 0,
|
||||
maxConcurrent: 15,
|
||||
serverInfo: {}, // Contents of the last received X-Dougal-Server HTTP header
|
||||
isGatewayReliable: null, // True if we start seeing HTTP 502‒504 responses
|
||||
});
|
||||
|
||||
export default state;
|
||||
|
||||
@@ -17,6 +17,7 @@ async function refreshEvents ({commit, dispatch, state, rootState}, [modifiedAft
|
||||
? `/project/${pid}/event/changes/${(new Date(modifiedAfter)).toISOString()}?unique=t`
|
||||
: `/project/${pid}/event`;
|
||||
const init = {
|
||||
cache: "reload",
|
||||
signal: state.loading.signal
|
||||
};
|
||||
const res = await dispatch('api', [url, init]);
|
||||
|
||||
@@ -11,6 +11,7 @@ async function refreshLabels ({commit, dispatch, state, rootState}) {
|
||||
const pid = rootState.project.projectId;
|
||||
const url = `/project/${pid}/label`;
|
||||
const init = {
|
||||
cache: "reload",
|
||||
signal: state.loading.signal
|
||||
};
|
||||
const res = await dispatch('api', [url, init]);
|
||||
|
||||
@@ -11,6 +11,7 @@ async function refreshLines ({commit, dispatch, state, rootState}) {
|
||||
const pid = rootState.project.projectId;
|
||||
const url = `/project/${pid}/line`;
|
||||
const init = {
|
||||
cache: "reload",
|
||||
signal: state.loading.signal
|
||||
};
|
||||
const res = await dispatch('api', [url, init]);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
function registerHandler({ commit }, { table, handler }) {
|
||||
commit('REGISTER_HANDLER', { table, handler });
|
||||
}
|
||||
|
||||
function unregisterHandler({ commit }, { table, handler }) {
|
||||
commit('UNREGISTER_HANDLER', { table, handler });
|
||||
}
|
||||
|
||||
function processServerEvent({ commit, dispatch, state, rootState }, message) {
|
||||
//console.log("processServerEvent", message);
|
||||
// Error handling for invalid messages
|
||||
if (!message) {
|
||||
console.error("processServerEvent called without arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message.channel) {
|
||||
console.error("processServerEvent message missing channel");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message.payload) {
|
||||
console.error("processServerEvent message missing payload");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.operation == "INSERT") {
|
||||
if (message.payload.new == null) {
|
||||
console.error("Expected payload.new to be non-null");
|
||||
return;
|
||||
}
|
||||
} else if (message.payload.operation == "UPDATE") {
|
||||
if (message.payload.old == null || message.payload.new == null) {
|
||||
console.error("Expected payload.old and paylaod.new to be non-null");
|
||||
return;
|
||||
}
|
||||
} else if (message.payload.operation == "DELETE") {
|
||||
if (message.payload.old == null) {
|
||||
console.error("Expected payload.old to be non-null");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.warn(`Unrecognised operation: ${message.payload.operation}`);
|
||||
}
|
||||
|
||||
const table = message.channel; // or message.payload?.table;
|
||||
//console.log("table=", table);
|
||||
if (!table || !state.handlers[table] || state.handlers[table].length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a debounced runner per table if not exists
|
||||
if (!state.debouncedRunners) {
|
||||
state.debouncedRunners = {}; // Not reactive needed? Or use Vue.set
|
||||
}
|
||||
if (!state.debouncedRunners[table]) {
|
||||
const config = {
|
||||
wait: 300, // min silence in ms
|
||||
maxWait: 1000, // max wait before force run, adjustable
|
||||
trailing: true,
|
||||
leading: false
|
||||
};
|
||||
state.debouncedRunners[table] = debounce((lastMessage) => {
|
||||
const context = { commit, dispatch, state: rootState, rootState }; // Approximate action context
|
||||
state.handlers[table].forEach(handler => {
|
||||
try {
|
||||
//console.log("Trying handler:", handler);
|
||||
handler(context, lastMessage);
|
||||
} catch (e) {
|
||||
console.error(`Error in handler for table ${table}:`, e);
|
||||
}
|
||||
});
|
||||
}, config.wait, { maxWait: config.maxWait });
|
||||
}
|
||||
|
||||
// Call the debounced function with the current message
|
||||
// Debounce will use the last call's argument if multiple
|
||||
state.debouncedRunners[table](message);
|
||||
}
|
||||
|
||||
export default { registerHandler, unregisterHandler, processServerEvent };
|
||||
|
||||
@@ -11,4 +11,29 @@ function setServerConnectionState (state, isConnected) {
|
||||
state.serverConnected = !!isConnected;
|
||||
}
|
||||
|
||||
export default { setServerEvent, clearServerEvent, setServerConnectionState };
|
||||
function REGISTER_HANDLER(state, { table, handler }) {
|
||||
if (!state.handlers[table]) {
|
||||
state.handlers[table] = [];
|
||||
}
|
||||
if (!state.handlers[table].includes(handler)) {
|
||||
state.handlers[table].push(handler);
|
||||
}
|
||||
}
|
||||
|
||||
function UNREGISTER_HANDLER(state, { table, handler }) {
|
||||
if (state.handlers[table]) {
|
||||
const handlerIndex = state.handlers[table].findIndex(el => el === handler);
|
||||
if (handlerIndex != -1) {
|
||||
state.handlers[table].splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
setServerEvent,
|
||||
clearServerEvent,
|
||||
setServerConnectionState,
|
||||
REGISTER_HANDLER,
|
||||
UNREGISTER_HANDLER
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const state = () => ({
|
||||
serverEvent: null,
|
||||
serverConnected: false
|
||||
serverConnected: false,
|
||||
handlers: {}, // table: array of functions (each fn receives { commit, dispatch, state, rootState, message })
|
||||
});
|
||||
|
||||
export default state;
|
||||
|
||||
@@ -11,6 +11,7 @@ async function refreshPlan ({commit, dispatch, state, rootState}) {
|
||||
const pid = rootState.project.projectId;
|
||||
const url = `/project/${pid}/plan`;
|
||||
const init = {
|
||||
cache: "reload",
|
||||
signal: state.loading.signal
|
||||
};
|
||||
const res = await dispatch('api', [url, init]);
|
||||
|
||||
@@ -1,18 +1,47 @@
|
||||
|
||||
|
||||
function transform (item) {
|
||||
item.ts0 = new Date(item.ts0);
|
||||
item.ts1 = new Date(item.ts1);
|
||||
return item;
|
||||
const newItem = {...item}
|
||||
newItem.ts0 = new Date(newItem.ts0);
|
||||
newItem.ts1 = new Date(newItem.ts1);
|
||||
return newItem;
|
||||
}
|
||||
|
||||
// ATTENTION: This relies on the new planner endpoint
|
||||
// as per issue #281.
|
||||
|
||||
function setRemarks (state, remarks) {
|
||||
state.remarks = remarks;
|
||||
}
|
||||
|
||||
function setSequence (state, sequence) {
|
||||
state.sequences.push(Object.freeze(transform(sequence)));
|
||||
}
|
||||
|
||||
function deleteSequence (state, sequence) {
|
||||
const seq = transform(sequence)
|
||||
const idx = state.sequences?.findIndex( s => Object.keys(seq).every( k => JSON.stringify(s[k]) == JSON.stringify(seq[k]) ));
|
||||
if (idx != -1) {
|
||||
state.sequences.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function replaceSequence (state, [oldSequence, newSequence]) {
|
||||
console.log("replaceSequence", oldSequence, newSequence);
|
||||
const seq = transform(oldSequence)
|
||||
const idx = state.sequences?.findIndex( s => Object.keys(seq).every( k => JSON.stringify(s[k]) == JSON.stringify(seq[k]) ));
|
||||
console.log("idx", idx);
|
||||
if (idx != -1) {
|
||||
state.sequences.splice(idx, 1, transform(newSequence))
|
||||
console.log("spliced in");
|
||||
}
|
||||
}
|
||||
|
||||
function setPlan (state, plan) {
|
||||
// We don't need or want the planned sequences array to be reactive
|
||||
state.sequences = Object.freeze(plan.sequences.map(transform));
|
||||
state.remarks = plan.remarks;
|
||||
state.sequences = [];
|
||||
plan.sequences.forEach( sequence => setSequence(state, sequence) );
|
||||
setRemarks(state, plan.remarks);
|
||||
}
|
||||
|
||||
function setPlanLoading (state, abortController = new AbortController()) {
|
||||
@@ -51,6 +80,10 @@ function abortPlanLoading (state) {
|
||||
}
|
||||
|
||||
export default {
|
||||
setRemarks,
|
||||
setSequence,
|
||||
deleteSequence,
|
||||
replaceSequence,
|
||||
setPlan,
|
||||
setPlanLoading,
|
||||
clearPlanLoading,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const state = () => ({
|
||||
sequences: Object.freeze([]),
|
||||
sequences: [],
|
||||
remarks: null,
|
||||
loading: null,
|
||||
timestamp: null,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
async function getProject ({commit, dispatch}, projectId) {
|
||||
const init = {
|
||||
headers: {
|
||||
cache: "reload",
|
||||
"If-None-Match": "" // Ensure we get a fresh response
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ async function refreshProjects ({commit, dispatch, state, rootState}) {
|
||||
const pid = rootState.project.projectId;
|
||||
const url = `/project`;
|
||||
const init = {
|
||||
cache: "reload",
|
||||
signal: state.loading.signal
|
||||
};
|
||||
const res = await dispatch('api', [url, init, null, {silent:true}]);
|
||||
|
||||
@@ -11,6 +11,7 @@ async function refreshSequences ({commit, dispatch, state, rootState}) {
|
||||
const pid = rootState.project.projectId;
|
||||
const url = `/project/${pid}/sequence?files=true`;
|
||||
const init = {
|
||||
cache: "reload",
|
||||
signal: state.loading.signal
|
||||
};
|
||||
const res = await dispatch('api', [url, init]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import jwt_decode from 'jwt-decode';
|
||||
import { User } from '@/lib/user';
|
||||
|
||||
async function login ({commit, dispatch}, loginRequest) {
|
||||
async function login ({ commit, dispatch }, loginRequest) {
|
||||
const url = "/login";
|
||||
const init = {
|
||||
method: "POST",
|
||||
@@ -9,93 +9,86 @@ async function login ({commit, dispatch}, loginRequest) {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: loginRequest
|
||||
};
|
||||
|
||||
const callback = async (err, res) => {
|
||||
if (!err && res) {
|
||||
const { token } = (await res.json());
|
||||
await dispatch('setCredentials', {token});
|
||||
}
|
||||
}
|
||||
const res = await dispatch('api', [url, init]);
|
||||
if (res && res.ok) {
|
||||
await dispatch('setCredentials', {force: true});
|
||||
await dispatch('loadUserPreferences');
|
||||
}
|
||||
|
||||
await dispatch('api', [url, init, callback]);
|
||||
await dispatch('loadUserPreferences');
|
||||
}
|
||||
|
||||
async function logout ({commit, dispatch}) {
|
||||
commit('setCookie', null);
|
||||
async function logout ({ commit, dispatch }) {
|
||||
commit('setToken', null);
|
||||
commit('setUser', null);
|
||||
// Should delete JWT cookie
|
||||
await dispatch('api', ["/logout"]);
|
||||
|
||||
// Clear preferences
|
||||
commit('setPreferences', {});
|
||||
}
|
||||
|
||||
function browserCookie (state) {
|
||||
return document.cookie.split(/; */).find(i => /^JWT=.+/.test(i));
|
||||
}
|
||||
function setCredentials({ state, commit, getters, dispatch, rootState }, { force, token, response } = {}) {
|
||||
try {
|
||||
let tokenValue = token;
|
||||
|
||||
function cookieChanged (cookie) {
|
||||
return browserCookie != cookie;
|
||||
}
|
||||
if (!tokenValue && response?.headers?.get('x-jwt')) {
|
||||
tokenValue = response.headers.get('x-jwt');
|
||||
}
|
||||
|
||||
function setCredentials ({state, commit, getters, dispatch, rootState}, {force, token} = {}) {
|
||||
if (token || force || cookieChanged(state.cookie)) {
|
||||
try {
|
||||
const cookie = browserCookie();
|
||||
const decoded = (token ?? cookie) ? jwt_decode(token ?? cookie.split("=")[1]) : null;
|
||||
commit('setCookie', (cookie ?? (token && ("JWT="+token))) || undefined);
|
||||
if (!tokenValue) {
|
||||
console.log('No JWT found in token or response');
|
||||
return;
|
||||
}
|
||||
|
||||
if (force || tokenValue !== getters.jwt) {
|
||||
const decoded = jwt_decode(tokenValue);
|
||||
commit('setToken', tokenValue);
|
||||
commit('setUser', decoded ? new User(decoded, rootState.api.api) : null);
|
||||
} catch (err) {
|
||||
if (err.name == "InvalidTokenError") {
|
||||
console.warn("Failed to decode", browserCookie());
|
||||
} else {
|
||||
console.error("setCredentials", err);
|
||||
}
|
||||
commit('setCookie', {name: "JWT", value: tokenValue, expires: (decoded.exp??0)*1000});
|
||||
|
||||
console.log('Credentials refreshed at', new Date().toISOString());
|
||||
} else {
|
||||
console.log('JWT unchanged, skipping update');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('setCredentials error:', err.message, 'token:', token, 'response:', response?.headers?.get('x-jwt'));
|
||||
if (err.name === 'InvalidTokenError') {
|
||||
commit('setToken', null);
|
||||
commit('setUser', null);
|
||||
commit('clearCookie', "JWT")
|
||||
}
|
||||
}
|
||||
dispatch('loadUserPreferences');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user preferences to localStorage and store.
|
||||
*
|
||||
* User preferences are identified by a key that gets
|
||||
* prefixed with the user ID. The value can
|
||||
* be anything that JSON.stringify can parse.
|
||||
*/
|
||||
function saveUserPreference ({state, commit}, [key, value]) {
|
||||
function saveUserPreference({ state, commit }, [key, value]) {
|
||||
const k = `${state.user?.id}.${key}`;
|
||||
|
||||
if (value !== undefined) {
|
||||
localStorage.setItem(k, JSON.stringify(value));
|
||||
|
||||
const preferences = state.preferences;
|
||||
preferences[key] = value;
|
||||
const preferences = { ...state.preferences, [key]: value };
|
||||
commit('setPreferences', preferences);
|
||||
} else {
|
||||
localStorage.removeItem(k);
|
||||
|
||||
const preferences = state.preferences;
|
||||
const preferences = { ...state.preferences };
|
||||
delete preferences[key];
|
||||
commit('setPreferences', preferences);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUserPreferences ({state, commit}) {
|
||||
// Get all keys which are of interest to us
|
||||
async function loadUserPreferences({ state, commit }) {
|
||||
const prefix = `${state.user?.id}`;
|
||||
const keys = Object.keys(localStorage).filter( k => k.startsWith(prefix) );
|
||||
|
||||
// Build the preferences object
|
||||
const keys = Object.keys(localStorage).filter(k => k.startsWith(prefix));
|
||||
const preferences = {};
|
||||
keys.map(str => {
|
||||
keys.forEach(str => {
|
||||
const value = JSON.parse(localStorage.getItem(str));
|
||||
const key = str.split(".").slice(2).join(".");
|
||||
preferences[key] = value;
|
||||
});
|
||||
|
||||
// Commit it
|
||||
commit('setPreferences', preferences);
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
login,
|
||||
logout,
|
||||
|
||||
@@ -4,9 +4,7 @@ function user (state) {
|
||||
}
|
||||
|
||||
function jwt (state) {
|
||||
if (state.cookie?.startsWith("JWT=")) {
|
||||
return state.cookie.substring(4);
|
||||
}
|
||||
return state.token;
|
||||
}
|
||||
|
||||
function preferences (state) {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
|
||||
function setCookie (state, cookie) {
|
||||
state.cookie = cookie;
|
||||
function setToken (state, token) {
|
||||
state.token = token;
|
||||
if (token) {
|
||||
localStorage?.setItem("jwt", token);
|
||||
} else {
|
||||
localStorage?.removeItem("jwt");
|
||||
}
|
||||
}
|
||||
|
||||
function setUser (state, user) {
|
||||
@@ -11,4 +16,18 @@ function setPreferences (state, preferences) {
|
||||
state.preferences = preferences;
|
||||
}
|
||||
|
||||
export default { setCookie, setUser, setPreferences };
|
||||
function setCookie (state, opts = {}) {
|
||||
const name = opts.name ?? "JWT";
|
||||
const value = opts.value ?? "";
|
||||
const expires = opts.expires ? (new Date(opts.expires)) : (new Date(0));
|
||||
const path = opts.path ?? "/";
|
||||
const sameSite = opts.sameSite ?? "Lax";
|
||||
|
||||
document.cookie = `${name}=${value};path=${path};SameSite=${sameSite};expires=${expires.toUTCString()}`;
|
||||
}
|
||||
|
||||
function clearCookie (state, name) {
|
||||
setCookie(state, {name});
|
||||
}
|
||||
|
||||
export default { setToken, setUser, setPreferences, setCookie, clearCookie };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const state = () => ({
|
||||
cookie: null,
|
||||
token: localStorage?.getItem("jwt") ?? null,
|
||||
user: null,
|
||||
preferences: {}
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
},
|
||||
|
||||
async refresh () {
|
||||
const text = await this.api([`/rss/?remote=${atob(this.$route.params.source)}`, {text:true}]);
|
||||
const text = await this.api([`/rss/?remote=${atob(this.$route.params.source)}`, {format:"text"}]);
|
||||
try {
|
||||
this.feed = this.parse(text);
|
||||
} catch (err) {
|
||||
|
||||
@@ -737,6 +737,13 @@ export default {
|
||||
if (event.id) {
|
||||
const id = event.id;
|
||||
delete event.id;
|
||||
|
||||
// If this is an edit, ensure that it is *either*
|
||||
// a timestamp event or a sequence + point one.
|
||||
if (event.sequence && event.point && event.tstamp) {
|
||||
delete event.tstamp;
|
||||
}
|
||||
|
||||
this.putEvent(id, event, callback); // No await
|
||||
} else {
|
||||
this.postEvent(event, callback); // No await
|
||||
@@ -829,7 +836,7 @@ export default {
|
||||
viewOnMap(item) {
|
||||
if (item?.meta && item.meta?.geometry?.type == "Point") {
|
||||
const [ lon, lat ] = item.meta.geometry.coordinates;
|
||||
return `map#15/${lon.toFixed(6)}/${lat.toFixed(6)}`;
|
||||
return `map#z15x${lon.toFixed(6)}y${lat.toFixed(6)}::${lon.toFixed(6)},${lat.toFixed(6)}`;
|
||||
} else if (item?.items) {
|
||||
return this.viewOnMap(item.items[0]);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,13 @@ export default {
|
||||
await this.logout();
|
||||
await this.login(this.credentials);
|
||||
|
||||
if (this.user) {
|
||||
console.log("Login successful");
|
||||
// Should trigger auto-refresh over ws as well as authenticating the
|
||||
// user over ws.
|
||||
this.$root.sendJwt();
|
||||
}
|
||||
|
||||
if (this.user && !this.user.autologin) {
|
||||
this.$router.replace("/");
|
||||
} else {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
761
lib/www/client/source/src/views/MapLayersMixin.vue
Normal file
761
lib/www/client/source/src/views/MapLayersMixin.vue
Normal file
@@ -0,0 +1,761 @@
|
||||
<script>
|
||||
// Important info about performance:
|
||||
// https://deck.gl/docs/developer-guide/performance#supply-attributes-directly
|
||||
|
||||
import { Deck, WebMercatorViewport, FlyToInterpolator, CompositeLayer } from '@deck.gl/core';
|
||||
import { GeoJsonLayer, LineLayer, PathLayer, BitmapLayer, ScatterplotLayer, ColumnLayer, IconLayer } from '@deck.gl/layers';
|
||||
import {HeatmapLayer} from '@deck.gl/aggregation-layers';
|
||||
import { TileLayer, MVTLayer, TripsLayer } from '@deck.gl/geo-layers';
|
||||
import { SimpleMeshLayer } from '@deck.gl/mesh-layers';
|
||||
import { OBJLoader } from '@loaders.gl/obj';
|
||||
|
||||
//import { json } from 'd3-fetch';
|
||||
import * as d3a from 'd3-array';
|
||||
|
||||
import { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved } from '@dougal/binary';
|
||||
import { DougalShotLayer } from '@/lib/deck.gl';
|
||||
import { DougalSequenceLayer, DougalEventsLayer } from '@/lib/deck.gl';
|
||||
import DougalBinaryLoader from '@/lib/deck.gl/DougalBinaryLoader';
|
||||
|
||||
import { colors } from 'vuetify/lib'
|
||||
|
||||
function hexToArray (hex, defaultValue = [ 0xc0, 0xc0, 0xc0, 0xff ]) {
|
||||
|
||||
if (typeof hex != "string" || hex.length < 6) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (hex[0] == "#") {
|
||||
hex = hex.slice(1); // remove the '#' character
|
||||
}
|
||||
|
||||
return [
|
||||
parseInt(hex.slice(0, 2), 16),
|
||||
parseInt(hex.slice(2, 4), 16),
|
||||
parseInt(hex.slice(4, 6), 16),
|
||||
hex.length > 6 ? parseInt(hex.slice(6, 8), 16) : 255
|
||||
];
|
||||
}
|
||||
|
||||
function namedColourToArray (name) {
|
||||
const parts = name.split(/\s+/).map( (s, i) =>
|
||||
i
|
||||
? s.replace("-", "")
|
||||
: s.replace(/-([a-z])/g, (match, group1) => group1.toUpperCase())
|
||||
);
|
||||
parts[0]
|
||||
if (parts.length == 1) parts[1] = "base";
|
||||
const hex = parts.reduce((acc, key) => acc[key], colors);
|
||||
return hexToArray(hex);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "MapLayersMixin",
|
||||
|
||||
data () {
|
||||
|
||||
return {
|
||||
|
||||
COLOUR_SCALE_1: [
|
||||
// negative
|
||||
[65, 182, 196],
|
||||
[127, 205, 187],
|
||||
[199, 233, 180],
|
||||
[237, 248, 177],
|
||||
|
||||
// positive
|
||||
[255, 255, 204],
|
||||
[255, 237, 160],
|
||||
[254, 217, 118],
|
||||
[254, 178, 76],
|
||||
[253, 141, 60],
|
||||
[252, 78, 42],
|
||||
[227, 26, 28],
|
||||
[189, 0, 38],
|
||||
[128, 0, 38]
|
||||
]
|
||||
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
normalisedColourScale(v, scale = this.COLOUR_SCALE_1, min = 0, max = 1) {
|
||||
const range = max-min;
|
||||
const i = Math.min(scale.length, Math.max(Math.round((v-min) / range * scale.length), 0));
|
||||
//console.log(`v=${v}, scale.length=${scale.length}, min=${min}, max=${max}, i=${i}, → ${scale[i]}`);
|
||||
return scale[i];
|
||||
},
|
||||
|
||||
|
||||
makeDataFromBinary ( {positions, values, udv} ) {
|
||||
const totalCount = positions.length / 2;
|
||||
|
||||
const attributes = {
|
||||
getPosition: {
|
||||
value: positions,
|
||||
type: 'float32',
|
||||
size: 2
|
||||
},
|
||||
udv
|
||||
};
|
||||
|
||||
values.forEach((valArray, k) => {
|
||||
let value = valArray;
|
||||
if (valArray instanceof BigUint64Array) {
|
||||
value = Float64Array.from(valArray, v => Number(v));
|
||||
}
|
||||
attributes[`value${k}`] = {
|
||||
value,
|
||||
type: value instanceof Float64Array ? 'float64' :
|
||||
value instanceof Uint16Array ? 'uint16' :
|
||||
value instanceof Uint32Array ? 'uint32' : 'float32',
|
||||
size: 1
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
length: totalCount,
|
||||
attributes
|
||||
};
|
||||
},
|
||||
|
||||
loadOptions (options = {}) {
|
||||
return {
|
||||
loadOptions: {
|
||||
fetch: {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.$store.getters.jwt}`,
|
||||
}
|
||||
},
|
||||
...options
|
||||
},
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
|
||||
osmLayer (options = {}) {
|
||||
return new TileLayer({
|
||||
id: "osm",
|
||||
// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
|
||||
data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
|
||||
minZoom: 0,
|
||||
maxZoom: 19,
|
||||
tileSize: 256,
|
||||
|
||||
renderSubLayers: props => {
|
||||
const {
|
||||
bbox: {west, south, east, north}
|
||||
} = props.tile;
|
||||
|
||||
return new BitmapLayer(props, {
|
||||
data: null,
|
||||
image: props.data,
|
||||
bounds: [west, south, east, north]
|
||||
});
|
||||
},
|
||||
...options
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
// OSM tiles layer. Handy to make water transparent
|
||||
// but not super reliable yet
|
||||
|
||||
osmVectorLayer (options = {}) {
|
||||
return new MVTLayer({
|
||||
id: 'osm',
|
||||
data: 'https://vector.openstreetmap.org/shortbread_v1/{z}/{x}/{y}.mvt',
|
||||
minZoom: 0,
|
||||
maxZoom: 14,
|
||||
getFillColor: feature => {
|
||||
const layer = feature.properties.layerName;
|
||||
//console.log("layer =", layer, feature.properties.kind);
|
||||
switch (layer) {
|
||||
case "ocean":
|
||||
return [0, 0, 0, 0];
|
||||
case "land":
|
||||
return [ 0x54, 0x6E, 0x7A, 255 ];
|
||||
default:
|
||||
return [ 240, 240, 240, 255 ];
|
||||
}
|
||||
},
|
||||
getLineColor: feature => {
|
||||
if (feature.properties.layer === 'water') {
|
||||
return [0, 0, 0, 0]; // No outline for water
|
||||
}
|
||||
return [192, 192, 192, 255]; // Default line color for roads, etc.
|
||||
},
|
||||
getLineWidth: feature => {
|
||||
if (feature.properties.highway) {
|
||||
return feature.properties.highway === 'motorway' ? 6 : 3; // Example road widths
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
stroked: true,
|
||||
filled: true,
|
||||
pickable: true
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
openSeaMapLayer (options = {}) {
|
||||
return new TileLayer({
|
||||
id: "sea",
|
||||
data: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
|
||||
|
||||
minZoom: 0,
|
||||
maxZoom: 19,
|
||||
tileSize: 256,
|
||||
|
||||
renderSubLayers: props => {
|
||||
const {
|
||||
bbox: {west, south, east, north}
|
||||
} = props.tile;
|
||||
|
||||
return new BitmapLayer(props, {
|
||||
data: null,
|
||||
image: props.data,
|
||||
bounds: [west, south, east, north]
|
||||
});
|
||||
},
|
||||
...options
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
// Norwegian nautical charts
|
||||
// As of 2025, not available for some weird reason
|
||||
nauLayer (options = {}) {
|
||||
return new TileLayer({
|
||||
id: "nau",
|
||||
// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
|
||||
data: 'https://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=sjokartraster&zoom={z}&x={x}&y={y}',
|
||||
|
||||
minZoom: 0,
|
||||
maxZoom: 19,
|
||||
tileSize: 256,
|
||||
|
||||
renderSubLayers: props => {
|
||||
const {
|
||||
bbox: {west, south, east, north}
|
||||
} = props.tile;
|
||||
|
||||
return new BitmapLayer(props, {
|
||||
data: null,
|
||||
image: props.data,
|
||||
bounds: [west, south, east, north]
|
||||
});
|
||||
},
|
||||
...options
|
||||
})
|
||||
},
|
||||
|
||||
vesselTrackPointsLayer (options = {}) {
|
||||
|
||||
if (!this.vesselPosition) return;
|
||||
|
||||
return new SimpleMeshLayer({
|
||||
id: 'navp',
|
||||
data: [ this.vesselPosition ],
|
||||
//getColor: [ 255, 48, 0 ],
|
||||
getColor: [ 174, 1, 174 ],
|
||||
getOrientation: d => [0, (270 - (d.heading ?? d.cmg ?? d.bearing ?? d.lineBearing ?? 0)) % 360 , 0],
|
||||
getPosition: d => [ d.x, d.y ],
|
||||
mesh: `/assets/boat0.obj`,
|
||||
sizeScale: 0.1,
|
||||
loaders: [OBJLoader],
|
||||
pickable: true,
|
||||
...options
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
vesselTrackLinesLayer (options = {}) {
|
||||
|
||||
const cfg = this.vesselTrackPeriodSettings[this.vesselTrackPeriod];
|
||||
|
||||
let ts1 = new Date(this.vesselTrackLastRefresh*1000);
|
||||
let ts0 = new Date(ts1.valueOf() - cfg.offset);
|
||||
let di = cfg.decimation;
|
||||
let l = 10000;
|
||||
|
||||
const breakLimit = (di ? di*20 : 5 * 60) * 1000;
|
||||
|
||||
let trailLength = (ts1 - ts0) / 1000;
|
||||
|
||||
return new TripsLayer({
|
||||
id: 'navl',
|
||||
data: `/api/vessel/track/?di=${di}&l=${l}&project=&ts0=${ts0.toISOString()}&ts1=${ts1.toISOString()}`,
|
||||
...this.loadOptions({
|
||||
fetch: {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.$store.getters.jwt}`,
|
||||
}
|
||||
}
|
||||
}),
|
||||
dataTransform: (data) => {
|
||||
if (data.length >= l) {
|
||||
console.warn(`Vessel track data may be truncated! Limit: ${l}`);
|
||||
}
|
||||
|
||||
const paths = [];
|
||||
let prevTstamp;
|
||||
paths.push({path: [], timestamps: [], num: 0, ts0: +Infinity, ts1: -Infinity});
|
||||
for (const el of data) {
|
||||
const tstamp = new Date(el.tstamp).valueOf();
|
||||
const curPath = () => paths[paths.length-1];
|
||||
if (prevTstamp && Math.abs(tstamp - prevTstamp) > breakLimit) {
|
||||
// Start a new path
|
||||
console.log(`Breaking path on interval ${Math.abs(tstamp - prevTstamp)} > ${breakLimit}`);
|
||||
paths.push({path: [], timestamps: [], num: paths.length, ts0: +Infinity, ts1: -Infinity});
|
||||
}
|
||||
|
||||
if (tstamp < curPath().ts0) {
|
||||
curPath().ts0 = tstamp;
|
||||
}
|
||||
if (tstamp > curPath().ts1) {
|
||||
curPath().ts1 = tstamp;
|
||||
}
|
||||
|
||||
curPath().path.push([el.x, el.y]);
|
||||
curPath().timestamps.push(tstamp/1000);
|
||||
prevTstamp = tstamp;
|
||||
}
|
||||
|
||||
paths.forEach (path => {
|
||||
path.nums = paths.length;
|
||||
path.ts0 = new Date(path.ts0);
|
||||
path.ts1 = new Date(path.ts1);
|
||||
});
|
||||
|
||||
return paths;
|
||||
},
|
||||
getPath: d => d.path,
|
||||
getTimestamps: d => d.timestamps,
|
||||
currentTime: ts1.valueOf() / 1000,
|
||||
trailLength,
|
||||
widthUnits: "meters",
|
||||
widthMinPixels: 4,
|
||||
getWidth: 10,
|
||||
getColor: [ 174, 1, 126, 200 ],
|
||||
stroked: true,
|
||||
pickable: true,
|
||||
...options
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
eventsLogLayer (options = {}) {
|
||||
|
||||
const labelColour = (d, i, t, c = [127, 65, 90]) => {
|
||||
const label = d?.properties?.labels?.[0];
|
||||
const colour = this.labels[label]?.view?.colour ?? "#cococo";
|
||||
|
||||
if (colour) {
|
||||
if (colour[0] == "#") {
|
||||
c = hexToArray(colour);
|
||||
} else {
|
||||
c = namedColourToArray(colour);
|
||||
}
|
||||
} else {
|
||||
//return [127, 65, 90];
|
||||
}
|
||||
|
||||
if (t != null) {
|
||||
c[3] = t;
|
||||
}
|
||||
|
||||
return c;
|
||||
};
|
||||
|
||||
return new DougalEventsLayer({
|
||||
id: 'log',
|
||||
data: `/api/project/${this.$route.params.project}/event?mime=application/geo%2Bjson`,
|
||||
...this.loadOptions(),
|
||||
lineWidthMinPixels: 2,
|
||||
getPosition: d => d.geometry.coordinates,
|
||||
jitter: 0.00015,
|
||||
getElevation: d => Math.min(Math.max(d.properties.remarks?.length || 10, 10), 200),
|
||||
getFillColor: (d, i) => labelColour(d, i, 200),
|
||||
getLineColor: (d, i) => labelColour(d, i, 200),
|
||||
radius: 0.001,
|
||||
radiusScale: 1,
|
||||
// This just won't work with radiusUnits = "pixels".
|
||||
// See: https://grok.com/share/c2hhcmQtMw%3D%3D_16578be4-20fd-4000-a765-f082503d0495
|
||||
radiusUnits: "pixels",
|
||||
radiusMinPixels: 1.5,
|
||||
radiusMaxPixels: 2.5,
|
||||
|
||||
pickable: true,
|
||||
...options
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
preplotSaillinesLinesLayer (options = {}) {
|
||||
return new GeoJsonLayer({
|
||||
id: 'psll',
|
||||
data: `/api/project/${this.$route.params.project}/gis/preplot/line?class=V&v=${this.lineTStamp?.valueOf()}`,
|
||||
...this.loadOptions(),
|
||||
lineWidthMinPixels: 1,
|
||||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||||
getLineWidth: 1,
|
||||
getPointRadius: 2,
|
||||
radiusUnits: "pixels",
|
||||
pointRadiusMinPixels: 2,
|
||||
pickable: true,
|
||||
...options
|
||||
})
|
||||
},
|
||||
|
||||
preplotLinesLayer (options = {}) {
|
||||
return new GeoJsonLayer({
|
||||
id: 'ppll',
|
||||
data: `/api/project/${this.$route.params.project}/gis/preplot/line?v=${this.lineTStamp?.valueOf()}`,
|
||||
...this.loadOptions(),
|
||||
lineWidthMinPixels: 1,
|
||||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||||
getLineWidth: 1,
|
||||
getPointRadius: 2,
|
||||
radiusUnits: "pixels",
|
||||
pointRadiusMinPixels: 2,
|
||||
pickable: true,
|
||||
...options
|
||||
})
|
||||
},
|
||||
|
||||
plannedLinesLinesLayer (options = {}) {
|
||||
return new PathLayer({
|
||||
id: 'planl',
|
||||
data: [...this.plannedSequences], // Create new array to trigger Deck.gl update
|
||||
dataTransform: (sequences) => {
|
||||
// Raise the data 10 m above ground so that it's visible over heatmaps, etc.
|
||||
return sequences.map( seq => ({
|
||||
...seq,
|
||||
geometry: {
|
||||
...seq.geometry,
|
||||
coordinates: seq.geometry.coordinates.map( pos => [...pos, 10] )
|
||||
}
|
||||
}))
|
||||
},
|
||||
getPath: d => d.geometry.coordinates,
|
||||
//getSourcePosition: d => d.geometry.coordinates[0],
|
||||
//getTargetPosition: d => d.geometry.coordinates[1],
|
||||
widthUnits: "meters",
|
||||
widthMinPixels: 4,
|
||||
getWidth: 25,
|
||||
//getLineWidth: 10,
|
||||
getColor: (d) => {
|
||||
const k = (d?.azimuth??0)/360*255;
|
||||
return [ k, 128, k, 200 ];
|
||||
},
|
||||
stroked: true,
|
||||
pickable: true,
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
rawSequencesLinesLayer (options = {}) {
|
||||
return new GeoJsonLayer({
|
||||
id: 'seqrl',
|
||||
data: `/api/project/${this.$route.params.project}/gis/raw/line?v=${this.sequenceTStamp?.valueOf()}`,
|
||||
...this.loadOptions(),
|
||||
lineWidthMinPixels: 1,
|
||||
getLineColor: (d) => d.properties.ntbp ? [0xe6, 0x51, 0x00, 200] : [0xff, 0x98, 0x00, 200],
|
||||
getLineWidth: 1,
|
||||
getPointRadius: 2,
|
||||
radiusUnits: "pixels",
|
||||
pointRadiusMinPixels: 2,
|
||||
pickable: true,
|
||||
...options
|
||||
})
|
||||
},
|
||||
|
||||
finalSequencesLinesLayer (options = {}) {
|
||||
return new GeoJsonLayer({
|
||||
id: 'seqfl',
|
||||
data: `/api/project/${this.$route.params.project}/gis/final/line?v=${this.sequenceTStamp?.valueOf()}`,
|
||||
...this.loadOptions(),
|
||||
lineWidthMinPixels: 1,
|
||||
getLineColor: (d) => d.properties.pending ? [0xa7, 0xff, 0xab, 200] : [0x00, 0x96, 0x88, 200],
|
||||
getLineWidth: 1,
|
||||
getPointRadius: 2,
|
||||
radiusUnits: "pixels",
|
||||
pointRadiusMinPixels: 2,
|
||||
pickable: true,
|
||||
...options
|
||||
})
|
||||
},
|
||||
|
||||
preplotSaillinesPointLayer (options = {}) {
|
||||
return new DougalSequenceLayer({
|
||||
id: 'pslp',
|
||||
data: `/api/project/${this.$route.params.project}/line/sail?v=${this.lineTStamp?.valueOf()}`, // API endpoint returning binary data
|
||||
loaders: [DougalBinaryLoader],
|
||||
...this.loadOptions({
|
||||
fetch: {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.$store.getters.jwt}`,
|
||||
Accept: 'application/vnd.aaltronav.dougal+octet-stream'
|
||||
}
|
||||
}
|
||||
}),
|
||||
getRadius: 2,
|
||||
getFillColor: (d, {data, index}) => data.attributes.value2.value[index] ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||||
//getFillColor: [0, 120, 220, 200],
|
||||
pickable: true,
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
preplotPointsLayer (options = {}) {
|
||||
return new DougalSequenceLayer({
|
||||
id: 'pplp',
|
||||
data: `/api/project/${this.$route.params.project}/line/source?v=${this.lineTStamp?.valueOf()}`, // API endpoint returning binary data
|
||||
loaders: [DougalBinaryLoader],
|
||||
...this.loadOptions({
|
||||
fetch: {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.$store.getters.jwt}`,
|
||||
Accept: 'application/vnd.aaltronav.dougal+octet-stream'
|
||||
}
|
||||
}
|
||||
}),
|
||||
getRadius: 2,
|
||||
getFillColor: (d, {data, index}) => data.attributes.value2.value[index] ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||||
//getFillColor: [0, 120, 220, 200],
|
||||
pickable: true,
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
plannedLinesPointsLayer (options = {}) {
|
||||
},
|
||||
|
||||
rawSequencesPointsLayer (options = {}) {
|
||||
|
||||
return new DougalSequenceLayer({
|
||||
id: 'seqrp',
|
||||
data: this.makeDataFromBinary(this.sequenceBinaryData),
|
||||
getRadius: 2,
|
||||
getFillColor: [0, 120, 220, 200],
|
||||
pickable: true,
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
finalSequencesPointsLayer (options = {}) {
|
||||
|
||||
return new DougalSequenceLayer({
|
||||
id: 'seqfp',
|
||||
data: this.makeDataFromBinary(this.sequenceBinaryDataFinal),
|
||||
getRadius: 2,
|
||||
getFillColor: [220, 120, 0, 200],
|
||||
pickable: true,
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
heatmapLayer(options = {}) {
|
||||
const { positions, values } = this.heatmapValue?.startsWith("co_")
|
||||
? this.sequenceBinaryDataFinal
|
||||
: this.sequenceBinaryData;
|
||||
|
||||
if (!positions?.length || !values?.length) {
|
||||
console.warn('No valid data for heatmapLayer');
|
||||
|
||||
return new HeatmapLayer({
|
||||
id: 'seqrh',
|
||||
data: [],
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let weights, offset = 0, scaler = 1;
|
||||
let colorDomain = null;
|
||||
let aggregation = "MEAN";
|
||||
let transform = (v) => v;
|
||||
|
||||
switch (this.heatmapValue) {
|
||||
case "total_error":
|
||||
weights = Float32Array.from(values[3], (ei, i) => {
|
||||
const ej = values[4][i];
|
||||
return Math.sqrt(ei * ei + ej * ej) / 100; // Euclidean distance in meters
|
||||
});
|
||||
colorDomain = [2, 20]; // scale: 1 (already divided by 100 above)
|
||||
break;
|
||||
case "delta_i":
|
||||
weights = values[3];
|
||||
scaler = 0.1;
|
||||
colorDomain = [100, 1200]; // scale: 100 (1 ‒ 12 m)
|
||||
break;
|
||||
case "delta_j":
|
||||
weights = values[4];
|
||||
scaler = 0.1;
|
||||
colorDomain = [10, 80]; // scale: 100 (0.1 ‒ 0.8 m)
|
||||
break;
|
||||
|
||||
case "co_total_error":
|
||||
weights = Float32Array.from(values[3], (ei, i) => {
|
||||
const ej = values[4][i];
|
||||
return Math.sqrt(ei * ei + ej * ej) / 100; // Euclidean distance in meters
|
||||
});
|
||||
colorDomain = [10, 150]; // Scale: 100 (0.1 ‒ 1 m)
|
||||
break;
|
||||
case "co_delta_i":
|
||||
weights = values[5];
|
||||
scaler = 0.1;
|
||||
colorDomain = [10, 150];
|
||||
break;
|
||||
case "co_delta_j":
|
||||
weights = values[6];
|
||||
scaler = 0.1;
|
||||
colorDomain = [0.2, 2];
|
||||
break;
|
||||
|
||||
case "delta_μ":
|
||||
weights = values[5];
|
||||
scaler = 0.1;
|
||||
break;
|
||||
case "delta_σ":
|
||||
weights = values[6];
|
||||
scaler = 0.1;
|
||||
colorDomain = [ 0.1, 1.5 ];
|
||||
break;
|
||||
case "delta_R":
|
||||
weights = values[7];
|
||||
scaler = 0.1;
|
||||
colorDomain = [ 0.5, 1.0 ];
|
||||
break;
|
||||
case "press_μ":
|
||||
weights = values[8];
|
||||
offset = -2000;
|
||||
colorDomain = [ 5, 50 ];
|
||||
break;
|
||||
case "press_σ":
|
||||
weights = values[9];
|
||||
colorDomain = [ 1, 19 ];
|
||||
break;
|
||||
case "press_R":
|
||||
weights = values[10];
|
||||
colorDomain = [ 3, 50 ];
|
||||
break;
|
||||
case "depth_μ":
|
||||
weights = values[11];
|
||||
offset = -6;
|
||||
scaler = 0.1;
|
||||
colorDomain = [ 0.1, 1 ];
|
||||
break;
|
||||
case "depth_σ":
|
||||
weights = values[12];
|
||||
scaler = 0.1;
|
||||
break;
|
||||
case "depth_R":
|
||||
weights = values[13];
|
||||
scaler = 0.1;
|
||||
break;
|
||||
case "fill_μ":
|
||||
weights = values[14];
|
||||
colorDomain = [ 300, 1000 ];
|
||||
break;
|
||||
case "fill_σ":
|
||||
weights = values[15];
|
||||
offset = -250;
|
||||
colorDomain = [ 0, 250 ];
|
||||
break;
|
||||
case "fill_R":
|
||||
weights = values[16];
|
||||
offset = -500;
|
||||
colorDomain = [ 0, 500 ];
|
||||
break;
|
||||
case "delay_μ":
|
||||
weights = values[17];
|
||||
offset = -150;
|
||||
colorDomain = [ 1.5, 25 ];
|
||||
//transform = (v) => {console.log("τ(v)", v); return v;};
|
||||
break;
|
||||
case "delay_σ":
|
||||
weights = values[18];
|
||||
break;
|
||||
case "delay_R":
|
||||
weights = values[19];
|
||||
break;
|
||||
case "no_fire":
|
||||
weights = values[20];
|
||||
transform = v => v >> 4;
|
||||
aggregation = "SUM";
|
||||
colorDomain = [ 0.1, 1.5 ];
|
||||
break;
|
||||
case "autofire":
|
||||
weights = values[20];
|
||||
transform = v => v & 0xf;
|
||||
aggregation = "SUM";
|
||||
colorDomain = [ 0.5, 1.5 ];
|
||||
break;
|
||||
case "misfire":
|
||||
weights = values[20];
|
||||
aggregation = "SUM";
|
||||
colorDomain = [ 0.5, 1.5 ];
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
const stats = {
|
||||
min: d3a.min(weights),
|
||||
mode: d3a.mode(weights),
|
||||
mean: d3a.mean(weights),
|
||||
max: d3a.max(weights),
|
||||
sd: d3a.deviation(weights),
|
||||
};
|
||||
const sr0 = [ stats.mean - 2.1*stats.sd, stats.mean + 2.1*stats.sd ];
|
||||
const sr1 = [ stats.mode - 2.1*stats.sd, stats.mode + 2.1*stats.sd ];
|
||||
|
||||
/*
|
||||
console.log('Positions sample:', positions.slice(0, 10));
|
||||
console.log('Weights sample:', weights.slice(0, 10));
|
||||
console.log("Mode:", this.heatmapValue);
|
||||
console.log('Weight stats:', stats);
|
||||
console.log("Suggested ranges");
|
||||
console.log(sr0);
|
||||
console.log(sr1);
|
||||
console.log("Actual ranges");
|
||||
console.log(colorDomain);
|
||||
*/
|
||||
|
||||
return new HeatmapLayer({
|
||||
id: 'seqrh',
|
||||
data: {
|
||||
length: weights.length,
|
||||
positions,
|
||||
weights
|
||||
/*
|
||||
attributes: {
|
||||
getPosition: { value: positions, type: 'float32', size: 2 },
|
||||
getWeight: { value: weights, type: 'float32', size: 1 }
|
||||
}
|
||||
*/
|
||||
},
|
||||
getPosition: (d, {index, data}) => [ data.positions[index*2], data.positions[index*2+1] ],
|
||||
getWeight: (d, {index, data}) => transform(Math.abs(data.weights[index] * scaler + offset)),
|
||||
colorDomain,
|
||||
radiusPixels: 25,
|
||||
aggregation,
|
||||
pickable: false,
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
259
lib/www/client/source/src/views/MapTooltipsMixin.vue
Normal file
259
lib/www/client/source/src/views/MapTooltipsMixin.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<script>
|
||||
import * as d3a from 'd3-array';
|
||||
|
||||
export default {
|
||||
name: "MapTooltipsMixin",
|
||||
|
||||
data () {
|
||||
return {
|
||||
tooltipDefaultStyle: { "max-width": "50ex"}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
getTooltip (args) {
|
||||
if (args?.layer?.constructor?.tooltip) {
|
||||
return args.layer.constructor.tooltip(args);
|
||||
} else if (args?.layer?.id == "seqrp" || args?.layer?.id == "seqfp") {
|
||||
return this.sequencePointsTooltip(args);
|
||||
} else if (args?.layer?.id == "log") {
|
||||
return this.eventLogTooltip(args);
|
||||
} else if (args?.layer?.id == "pplp" || args?.layer?.id == "pslp") {
|
||||
return this.preplotPointsTooltip(args);
|
||||
} else if (args?.layer?.id == "ppll" || args?.layer?.id == "psll") {
|
||||
return this.preplotLinesTooltip(args);
|
||||
} else if (args?.layer?.id == "planl") {
|
||||
return this.plannedLinesTooltip(args);
|
||||
} else if (args?.layer?.id == "seqrl" || args?.layer?.id == "seqfl") {
|
||||
return this.sequenceLinesTooltip(args);
|
||||
} else if (args?.layer?.id == "navp") {
|
||||
return this.vesselTrackPointsTooltip(args);
|
||||
} else if (args?.layer?.id == "navl") {
|
||||
return this.vesselTrackLinesTooltip(args);
|
||||
}
|
||||
},
|
||||
|
||||
preplotPointsTooltip (args) {
|
||||
const p = args?.object;
|
||||
if (p) {
|
||||
let html = "Preplot<br/>\n";
|
||||
|
||||
if ("sailline" in p) {
|
||||
// If there is a "sailline" attribute, this is actually a source line
|
||||
// "i" is the source line number, "sailline" the sail line number.
|
||||
html += `S${String(p.i).padStart(4, "0")}<br/>\n`;
|
||||
html += `V${String(p.sailline).padStart(4, "0")}<br/>\n`;
|
||||
} else {
|
||||
html += `V${String(p.i).padStart(4, "0")}<br/>\n`;
|
||||
}
|
||||
html += `P${String(p.j).padStart(4, "0")}<br/>\n`;
|
||||
|
||||
if (p.sailline_ntba) {
|
||||
html += `<b>Line <abbr title="Not to be acquired">NTBA</abbr></b><br/>\n`;
|
||||
}
|
||||
|
||||
if (p.ntba) {
|
||||
html += `<b>Point <abbr title="Not to be acquired">NTBA</abbr></b><br/>\n`;
|
||||
}
|
||||
|
||||
return {html, style: this.tooltipDefaultStyle};
|
||||
}
|
||||
},
|
||||
|
||||
preplotLinesTooltip (args) {
|
||||
const p = args?.object?.properties;
|
||||
if (p) {
|
||||
const lineType = args.layer.id == "psll" ? "Sailline" : "Source line";
|
||||
const direction = p.incr ? "▲" : "▼";
|
||||
let html = "";
|
||||
|
||||
html += `L${String(p.line).padStart(4, "0")} ${direction}<br/>\n`;
|
||||
html += `${lineType}<br/>\n`;
|
||||
|
||||
if (p.ntba) {
|
||||
html += "<b>Not to be acquired (NTBA)</b><br/>\n";
|
||||
}
|
||||
|
||||
if (p.remarks) {
|
||||
html += `<hr/>\n${this.$root.markdown(p.remarks)}\n`;
|
||||
}
|
||||
|
||||
return {html, style: this.tooltipDefaultStyle};
|
||||
}
|
||||
},
|
||||
|
||||
sequenceLinesTooltip (args) {
|
||||
let type;
|
||||
switch(args.layer.id) {
|
||||
case "seqrl":
|
||||
type = "Raw";
|
||||
break;
|
||||
case "seqfl":
|
||||
type = "Final";
|
||||
break;
|
||||
}
|
||||
|
||||
const p = args?.object?.properties;
|
||||
if (p) {
|
||||
let html = `Sequence ${p.sequence} (${type})<br/>\n`;
|
||||
html += `Line <b>${p.line}</b><br/>\n`;
|
||||
html += `${p.num_points} points (${p.missing_shots ? (p.missing_shots + " missing") : "None missing"})<br/>\n`;
|
||||
html+= `${(p.length??0).toFixed(0)} m ${(p.azimuth??0).toFixed(1)}°<br/>\n`;
|
||||
html += `${p.duration}<br/>\n`;
|
||||
html += `<b>${p.fsp}</b> @ ${(new Date(p.ts0))?.toISOString()}<br/>\n`;
|
||||
html += `<b>${p.lsp}</b> @ ${(new Date(p.ts1))?.toISOString()}<br/>\n`;
|
||||
if (p.ntbp) {
|
||||
html += "<b>Not to be processed</b><br/>\n";
|
||||
} else if (p.pending) {
|
||||
html += "<b>Pending</b><br/>\n";
|
||||
}
|
||||
html += "<hr/><br/>\n";
|
||||
html += this.$root.markdown(p.remarks);
|
||||
|
||||
return {html, style: this.tooltipDefaultStyle};
|
||||
}
|
||||
},
|
||||
|
||||
sequencePointsTooltip (args) {
|
||||
|
||||
const p = args?.object;
|
||||
|
||||
if (p) {
|
||||
|
||||
let html = "";
|
||||
|
||||
if (p.udv == 2) { // FIXME Must change this to something more meaningful
|
||||
// This is a shot info record:
|
||||
|
||||
html += `S${p.i.toString().padStart(3, "0")} ${p.j}</br>\n`;
|
||||
html += `<small>${new Date(Number(p.ts)).toISOString()}</small><br/>\n`;
|
||||
html += `Δj: ${p.εj.toFixed(2)} m / Δi: ${p.εi.toFixed(2)} m<br/>\n`;
|
||||
html += `<hr/>\n`;
|
||||
if (p.nofire) {
|
||||
html += `<b>No fire: ${p.nofire} guns</b><br/>\n`;
|
||||
}
|
||||
if (p.autofire) {
|
||||
html += `<b>Autofire: ${p.autofire} guns</b><br/>\n`;
|
||||
}
|
||||
html += `P: ${p.press_μ} psi ±${p.press_σ} psi (R=${p.press_R})<br/>\n`;
|
||||
html += `Δ: ${p.delta_μ} ms ±${p.delta_σ} ms (R=${p.delta_R})<br/>\n`;
|
||||
html += `D: ${p.depth_μ} m ±${p.depth_σ} m (R=${p.depth_R})<br/>\n`;
|
||||
html += `F: ${p.fill_μ} ms ±${p.fill_σ} ms (R=${p.fill_R})<br/>\n`;
|
||||
html += `W: ${p.delay_μ} ms ±${p.delay_σ} ms (R=${p.delay_R})<br/>\n`;
|
||||
|
||||
return {html, style: this.tooltipDefaultStyle};
|
||||
} else if (p.udv == 3) {
|
||||
// A final points record
|
||||
|
||||
html += `S${p.i.toString().padStart(3, "0")} ${p.j} (final)</br>\n`;
|
||||
html += `<small>${new Date(Number(p.ts)).toISOString()}</small><br/>\n`;
|
||||
html += `Δj: ${p.εj.toFixed(2)} m / Δi: ${p.εi.toFixed(2)} m (final−preplot)<br/>\n`;
|
||||
html += `δj: ${p.co_j.toFixed(2)} m / δi: ${p.co_i.toFixed(2)} m (final−raw)<br/>\n`;
|
||||
|
||||
return {html, style: this.tooltipDefaultStyle};
|
||||
}
|
||||
}
|
||||
console.log("no tooltip");
|
||||
|
||||
},
|
||||
|
||||
eventLogTooltip (args) {
|
||||
const p = args?.object?.properties;
|
||||
if (p) {
|
||||
let html = "";
|
||||
if (p.sequence && p.point) {
|
||||
html += `S${p.sequence.toString().padStart(3, "0")} ${p.point}</br>\n`;
|
||||
}
|
||||
html += `<small>${p.tstamp}</small><br/>\n`;
|
||||
html += `<span>${p.remarks}</span>`;
|
||||
if (p.labels?.length) {
|
||||
html += `</br>\n<small><i>${p.labels.join(", ")}</i></small>`;
|
||||
}
|
||||
|
||||
return {html, style: this.tooltipDefaultStyle};
|
||||
}
|
||||
},
|
||||
|
||||
plannedLinesTooltip (args) {
|
||||
const p = args?.object;
|
||||
if (p) {
|
||||
const duration = `${(p.duration?.hours??0).toString().padStart(2, "0")}:${(p.duration?.minutes??0).toString().padStart(2, "0")}`;
|
||||
const Δt = ((new Date(p.ts1)).valueOf() - (new Date(p.ts0)).valueOf()) / 1000; // seconds
|
||||
const speed = (p.length / Δt) * 3.6 / 1.852; // knots
|
||||
|
||||
let html = `Planned sequence <b>${p.sequence}</b></br>\n` +
|
||||
`Line <b>${p.line}</b> – ${p.name}</br>\n` +
|
||||
`${p.num_points} Points</br>\n` +
|
||||
`${p.length.toFixed(0)} m ${p.azimuth.toFixed(2)}°</br>\n` +
|
||||
`${duration} @ ${speed.toFixed(1)} kt</br>\n` +
|
||||
`<b>${p.fsp}</b> @ ${(new Date(p.ts0))?.toISOString()}</br>\n` +
|
||||
`<b>${p.lsp}</b> @ ${(new Date(p.ts1))?.toISOString()}`;
|
||||
|
||||
if (p.remarks) {
|
||||
html += `</br>\n<hr/>${this.$root.markdown(p.remarks)}`;
|
||||
}
|
||||
|
||||
return {html, style: this.tooltipDefaultStyle};
|
||||
}
|
||||
},
|
||||
|
||||
vesselTrackPointsTooltip (args) {
|
||||
const p = args.object;
|
||||
|
||||
if (p) {
|
||||
let html = `${p.vesselName}<br/>\n`
|
||||
+ `${p.tstamp}<br/>\n`
|
||||
+ `BSP ${(p.speed??0).toFixed(1)} kt CMG ${(p.cmg??0).toFixed(1).padStart(5, "0")}° HDG ${(p.bearing??0).toFixed(1).padStart(5, "0")}° DPT ${(p.waterDepth??0).toFixed(1)} m<br/>\n`
|
||||
+ `${p.lineStatus}<br/>\n`;
|
||||
|
||||
if (p.guns) {
|
||||
console.log(p);
|
||||
const pressure = p.guns.map( i => i[11] ); // 11 is gun pressure
|
||||
const μpress = d3a.mean(pressure);
|
||||
const σpress = d3a.deviation(pressure);
|
||||
|
||||
if (p.lineStatus && p.lineStatus != "offline") {
|
||||
html += `${p.lineName}<br/>\n`
|
||||
+ `S: ${p._sequence} L: ${p._line} P: ${p.shot}<br/>`
|
||||
+ `Source ${p.src_number} `
|
||||
+ ((p.trg_mode && p.trg_mode != "external") ? `<b>${p.trg_mode.toUpperCase()} TRIGGER</b> ` : "")
|
||||
+ `<small>FSID ${p.fsid}</small> <small>mask ${p.mask}</small><br/>\n`
|
||||
+ `Δ ${(p.avg_delta??0).toFixed(3)} ms ±${(p.std_delta??0).toFixed(3)} ms<br/>\n`
|
||||
+ `${(μpress??0).toFixed(0)} psi ±${(σpress??0).toFixed(0)} psi / ${(p.volume??0).toFixed(0)} in³<br/>\n`
|
||||
+ `along ${(p.inline??0).toFixed(1)} m / across ${(p.crossline??0).toFixed(1)} m<br/>\n`;
|
||||
} else {
|
||||
// Soft start?
|
||||
html +=
|
||||
`Source ${p.src_number} `
|
||||
+ ((p.trg_mode && p.trg_mode != "external") ? `<b>${p.trg_mode.toUpperCase()} TRIGGER</b> ` : "")
|
||||
+ `<small>mask ${p.mask}</small><br/>\n`
|
||||
+ `Δ ${(p.avg_delta??0).toFixed(3)} ms ±${(p.std_delta??0).toFixed(3)} ms<br/>\n`
|
||||
+ `${(p.manifold??0).toFixed(0)} psi / ${(p.volume??0).toFixed(0)} in³<br/>\n`
|
||||
+ `along ${(p.inline??0).toFixed(1)} m / across ${(p.crossline??0).toFixed(1)} m<br/>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return {html, style: this.tooltipDefaultStyle};
|
||||
}
|
||||
},
|
||||
|
||||
vesselTrackLinesTooltip (args) {
|
||||
const p = args.object;
|
||||
|
||||
console.log("track lines tooltip", p);
|
||||
|
||||
if (p) {
|
||||
|
||||
let html = `Segment ${p.num+1} / ${p.nums}<br/>\n`
|
||||
html += `${p.ts0.toISOString()}<br/>\n`
|
||||
html += `${p.ts1.toISOString()}<br/>\n`;
|
||||
|
||||
return {html, style: this.tooltipDefaultStyle};
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -43,48 +43,114 @@ export default {
|
||||
return this.loading || this.projectId;
|
||||
},
|
||||
|
||||
...mapGetters(["loading", "projectId", "projectSchema", "serverEvent"])
|
||||
},
|
||||
|
||||
watch: {
|
||||
async serverEvent (event) {
|
||||
if (event.channel == "project" && event.payload?.operation == "DELETE" && event.payload?.schema == "public") {
|
||||
// Project potentially deleted
|
||||
await this.getProject(this.$route.params.project);
|
||||
} else if (event.payload?.schema == this.projectSchema) {
|
||||
if (event.channel == "event") {
|
||||
this.refreshEvents();
|
||||
} else if (event.channel == "planned_lines") {
|
||||
this.refreshPlan();
|
||||
} else if (["raw_lines", "raw_shots", "final_lines", "final_shots"].includes(event.channel)) {
|
||||
this.refreshSequences();
|
||||
} else if (["preplot_lines", "preplot_points"].includes(event.channel)) {
|
||||
this.refreshLines();
|
||||
} else if (event.channel == "info") {
|
||||
if ((event.payload?.new ?? event.payload?.old)?.key == "plan") {
|
||||
this.refreshPlan();
|
||||
}
|
||||
} else if (event.channel == "project") {
|
||||
this.getProject(this.$route.params.project);
|
||||
}
|
||||
}
|
||||
}
|
||||
...mapGetters(["loading", "projectId", "projectSchema"])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
handleEvents (context, {payload}) {
|
||||
if (payload.pid != this.projectId) {
|
||||
console.warn(`${this.projectId} ignoring notification for ${payload.pid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshEvents();
|
||||
},
|
||||
|
||||
handleLines (context, {payload}) {
|
||||
if (payload.pid != this.projectId) {
|
||||
console.warn(`${this.projectId} ignoring notification for ${payload.pid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshLines();
|
||||
},
|
||||
|
||||
handlePlannedLines (context, {payload}) {
|
||||
if (payload.pid != this.projectId) {
|
||||
console.warn(`${this.projectId} ignoring notification for ${payload.pid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshPlan();
|
||||
},
|
||||
|
||||
handleSequences (context, {payload}) {
|
||||
if (payload.pid != this.projectId) {
|
||||
console.warn(`${this.projectId} ignoring notification for ${payload.pid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("handleSequences");
|
||||
this.refreshSequences();
|
||||
},
|
||||
|
||||
registerNotificationHandlers (action = "registerHandler") {
|
||||
|
||||
|
||||
this.$store.dispatch(action, {
|
||||
table: 'event',
|
||||
|
||||
handler: (context, message) => {
|
||||
this.handleEvents(context, message);
|
||||
}
|
||||
});
|
||||
|
||||
["preplot_lines", "preplot_points"].forEach( table => {
|
||||
this.$store.dispatch(action, {
|
||||
table,
|
||||
|
||||
handler: (context, message) => {
|
||||
this.handleLines(context, message);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
this.$store.dispatch(action, {
|
||||
table: 'planned_lines',
|
||||
|
||||
handler: (context, message) => {
|
||||
this.handlePlannedLines(context, message);
|
||||
}
|
||||
});
|
||||
|
||||
["raw_lines", "raw_shots", "final_lines", "final_shots"].forEach( table => {
|
||||
this.$store.dispatch(action, {
|
||||
table,
|
||||
|
||||
handler: (context, message) => {
|
||||
this.handleSequences(context, message);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
unregisterNotificationHandlers () {
|
||||
return this.registerNotificationHandlers("unregisterHandler");
|
||||
},
|
||||
|
||||
...mapActions(["getProject", "refreshLines", "refreshSequences", "refreshEvents", "refreshLabels", "refreshPlan"])
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
await this.getProject(this.$route.params.project);
|
||||
if (this.projectFound) {
|
||||
this.registerNotificationHandlers();
|
||||
|
||||
this.refreshLines();
|
||||
this.refreshSequences();
|
||||
this.refreshEvents();
|
||||
this.refreshLabels();
|
||||
this.refreshPlan();
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.unregisterNotificationHandlers();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
},
|
||||
],
|
||||
items: [],
|
||||
options: {},
|
||||
options: { sortBy: ["pid"], sortDesc: [true] },
|
||||
|
||||
// Whether or not to show archived projects
|
||||
showArchived: true,
|
||||
@@ -184,17 +184,7 @@ export default {
|
||||
: this.items.filter(i => !i.archived);
|
||||
},
|
||||
|
||||
...mapGetters(['loading', 'serverEvent', 'projects'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
async serverEvent (event) {
|
||||
if (event.channel == "project" && event.payload?.schema == "public") {
|
||||
if (event.payload?.operation == "DELETE" || event.payload?.operation == "INSERT") {
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
}
|
||||
...mapGetters(['loading', 'projects'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -220,6 +210,7 @@ export default {
|
||||
},
|
||||
|
||||
async load () {
|
||||
await this.refreshProjects();
|
||||
await this.list();
|
||||
const promises = [];
|
||||
for (const key in this.items) {
|
||||
@@ -234,6 +225,18 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
registerNotificationHandlers () {
|
||||
this.$store.dispatch('registerHandler', {
|
||||
table: 'project`',
|
||||
|
||||
handler: (context, message) => {
|
||||
if (message.payload?.table == "public") {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
contextMenu (e, {item}) {
|
||||
e.preventDefault();
|
||||
this.contextMenuShow = false;
|
||||
@@ -372,10 +375,11 @@ export default {
|
||||
|
||||
},
|
||||
|
||||
...mapActions(["api", "showSnack"])
|
||||
...mapActions(["api", "showSnack", "refreshProjects"])
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.registerNotificationHandlers();
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,7 +822,7 @@ export default {
|
||||
|
||||
async getNumLines () {
|
||||
const projectInfo = await this.api([`/project/${this.$route.params.project}`]);
|
||||
this.num_rows = projectInfo.sequences;
|
||||
this.num_rows = projectInfo?.sequences ?? 0;
|
||||
},
|
||||
|
||||
async fetchSequences (opts = {}) {
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
|
||||
<template v-slot:item.position="{item}">
|
||||
<a v-if="position(item).latitude"
|
||||
:href="`/projects/${$route.params.project}/map#15/${position(item).longitude}/${position(item).latitude}`"
|
||||
:href="`/projects/${$route.params.project}/map#z15x${position(item).longitude}y${position(item).latitude}:+seqrp;+psll:${position(item).longitude},${position(item).latitude}:S${String(item.sequence).padStart(3, '0')} ${item.point}`"
|
||||
title="View on map"
|
||||
:target="`/projects/${$route.params.project}/map`"
|
||||
@click.stop
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const webpack = require('webpack');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const gitDescribe = execSync('git describe --tags --always', { encoding: 'utf8' }).trim();
|
||||
|
||||
module.exports = {
|
||||
"transpileDependencies": [
|
||||
@@ -21,10 +24,10 @@ module.exports = {
|
||||
},
|
||||
proxy: {
|
||||
"^/api(/|$)": {
|
||||
target: "http://127.0.0.1:3000",
|
||||
target: process.env.DOUGAL_API_URL || "http://127.0.0.1:3000",
|
||||
},
|
||||
"^/ws(/|$)": {
|
||||
target: "ws://127.0.0.1:3000",
|
||||
target: process.env.DOUGAL_API_URL?.replace(/^http(s?):/i, "ws$1:") || "ws://127.0.0.1:3000",
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
@@ -48,7 +51,11 @@ module.exports = {
|
||||
// https://github.com/webpack/changelog-v5/issues/10
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
})
|
||||
}),
|
||||
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.DOUGAL_FRONTEND_VERSION': JSON.stringify(gitDescribe),
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
|
||||
@@ -69,6 +69,7 @@ const allMeta = (key, value) => {
|
||||
return { all: [ meta(key, value) ] };
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// NOTICE These routes do not require authentication
|
||||
//
|
||||
@@ -83,7 +84,10 @@ app.map({
|
||||
post: [ mw.user.logout ]
|
||||
},
|
||||
'/version': {
|
||||
get: [ mw.auth.operations, mw.version.get ]
|
||||
get: [ mw.auth.operations, mw.version.get ],
|
||||
'/history': {
|
||||
get: [ /*mw.auth.user,*/ mw.version.history.get ],
|
||||
}
|
||||
},
|
||||
'/': {
|
||||
get: [ mw.openapi.get ]
|
||||
@@ -100,6 +104,9 @@ app.use(mw.auth.access.user);
|
||||
// Don't process the request if the data hasn't changed
|
||||
app.use(mw.etag.ifNoneMatch);
|
||||
|
||||
// Use compression across the board
|
||||
app.use(mw.compress);
|
||||
|
||||
// We must be authenticated before we can access these
|
||||
app.map({
|
||||
'/project': {
|
||||
@@ -114,10 +121,12 @@ app.map({
|
||||
get: [ mw.auth.access.read, mw.project.summary.get ],
|
||||
},
|
||||
'/project/:project/configuration': {
|
||||
get: [ mw.project.configuration.get ], // Get project configuration
|
||||
patch: [ mw.auth.access.edit, mw.project.configuration.patch ], // Modify project configuration
|
||||
put: [ mw.auth.access.edit, mw.project.configuration.put ], // Overwrite configuration
|
||||
},
|
||||
'/project/:project/configuration/:path(*)?': {
|
||||
get: [ mw.auth.access.read, mw.configuration.get ],
|
||||
},
|
||||
|
||||
/*
|
||||
* GIS endpoints
|
||||
@@ -152,10 +161,13 @@ app.map({
|
||||
'/project/:project/line/': {
|
||||
get: [ mw.auth.access.read, mw.line.list ],
|
||||
},
|
||||
'/project/:project/line/:line': {
|
||||
// get: [ mw.auth.access.read, mw.line.get ],
|
||||
'/project/:project/line/:line(\\d+)': {
|
||||
get: [ mw.auth.access.read, mw.line.get ],
|
||||
patch: [ mw.auth.access.write, mw.line.patch ],
|
||||
},
|
||||
'/project/:project/line/:class(sail|source)': {
|
||||
get: [ mw.auth.access.read, mw.line.get ],
|
||||
},
|
||||
|
||||
/*
|
||||
* Sequence endpoints
|
||||
@@ -213,16 +225,28 @@ app.map({
|
||||
'changes/:since': {
|
||||
get: [ mw.auth.access.read, mw.event.changes ]
|
||||
},
|
||||
// TODO Rename -/:sequence → sequence/:sequence
|
||||
// NOTE: old alias for /sequence/:sequence
|
||||
'-/:sequence/': { // NOTE: We need to avoid conflict with the next endpoint ☹
|
||||
get: [ mw.auth.access.read, mw.event.sequence.get ],
|
||||
},
|
||||
':id/': {
|
||||
'sequence/:sequence/': {
|
||||
get: [ mw.auth.access.read, mw.event.sequence.get ],
|
||||
},
|
||||
':id(\\d+)/': {
|
||||
get: [ mw.auth.access.read, mw.event.get ],
|
||||
put: [ mw.auth.access.write, mw.event.put ],
|
||||
patch: [ mw.auth.access.write, mw.event.patch ],
|
||||
delete: [mw.auth.access.write, mw.event.delete ]
|
||||
},
|
||||
'import': {
|
||||
put: [ mw.auth.access.write, mw.event.import.csv, mw.event.import.put ],
|
||||
post: [ mw.auth.access.write, mw.event.import.csv, mw.event.import.put ],
|
||||
'/:filename': {
|
||||
put: [ mw.auth.access.read, mw.event.import.csv, mw.event.import.put ],
|
||||
post: [ mw.auth.access.write, mw.event.import.csv, mw.event.import.put ],
|
||||
delete: [ mw.auth.access.write, mw.event.import.delete ]
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -262,10 +286,6 @@ app.map({
|
||||
'/project/:project/label/': {
|
||||
get: [ mw.auth.access.read, mw.label.list ],
|
||||
// post: [ mw.label.post ],
|
||||
},
|
||||
'/project/:project/configuration/:path(*)?': {
|
||||
get: [ mw.auth.access.read, mw.configuration.get ],
|
||||
// post: [ mw.auth.access.admin, mw.label.post ],
|
||||
},
|
||||
'/project/:project/info/:path(*)': {
|
||||
get: [ mw.auth.operations, mw.auth.access.read, mw.info.get ],
|
||||
@@ -294,10 +314,10 @@ app.map({
|
||||
// // delete: [ mw.permissions.delete ]
|
||||
// },
|
||||
'/project/:project/files/:path(*)': {
|
||||
get: [ mw.auth.access.write, mw.files.get ]
|
||||
get: [ mw.files.get ]
|
||||
},
|
||||
'/files/?:path(*)': {
|
||||
get: [ mw.auth.access.write, mw.etag.noSave, mw.files.get ]
|
||||
get: [ mw.etag.noSave, mw.files.get ]
|
||||
},
|
||||
'/navdata/': { // TODO These endpoints should probably need read access auth
|
||||
get: [ mw.etag.noSave, mw.navdata.get ],
|
||||
@@ -305,6 +325,30 @@ app.map({
|
||||
get: [ mw.etag.noSave, mw.gis.navdata.get ]
|
||||
}
|
||||
},
|
||||
'/vessel/track': {
|
||||
get: [ /*mw.etag.noSave,*/ mw.vessel.track.get ], // JSON array
|
||||
'/line': {
|
||||
get: [ // GeoJSON Feature: type = LineString
|
||||
//mw.etag.noSave,
|
||||
(req, res, next) => { req.query.geojson = 'LineString'; next(); },
|
||||
mw.vessel.track.get
|
||||
]
|
||||
},
|
||||
'/point': {
|
||||
get: [ // GeoJSON FeatureCollection: feature types = Point
|
||||
//mw.etag.noSave,
|
||||
(req, res, next) => { req.query.geojson = 'Point'; next(); },
|
||||
mw.vessel.track.get
|
||||
]
|
||||
},
|
||||
'/points': {
|
||||
get: [ // JSON array of (Feature: type = Point)
|
||||
mw.etag.noSave,
|
||||
(req, res, next) => { req.query.geojson = true; next(); },
|
||||
mw.vessel.track.get
|
||||
],
|
||||
},
|
||||
},
|
||||
'/info/': {
|
||||
':path(*)': {
|
||||
get: [ mw.auth.operations, mw.info.get ],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { projectOrganisations, vesselOrganisations/*, orgAccess */} = require('../../../lib/db/project/organisations');
|
||||
const ServerUser = require('../../../lib/db/user/User');
|
||||
const { Organisations } = require('@dougal/organisations');
|
||||
const { ERROR, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
|
||||
|
||||
/** Second-order function.
|
||||
* Returns a middleware that checks if the user has access to
|
||||
@@ -14,11 +15,7 @@ function operation (operation) {
|
||||
if (req.params.project) {
|
||||
const projectOrgs = new Organisations(await projectOrganisations(req.params.project));
|
||||
const availableOrgs = projectOrgs.accessToOperation(operation).filter(user.organisations);
|
||||
console.log("Operation: ", operation);
|
||||
console.log("User: ", user.name);
|
||||
console.log("User orgs: ", user.organisations);
|
||||
console.log("Project orgs: ", projectOrgs);
|
||||
console.log("Available orgs: ", availableOrgs);
|
||||
DEBUG(`operation = ${operation}, user = ${user?.name}, user orgs = %j, project orgs = %j, availableOrgs = %j`, user.organisations.toJSON(), projectOrgs.toJSON(), availableOrgs.toJSON());
|
||||
if (availableOrgs.length > 0) {
|
||||
next();
|
||||
return;
|
||||
@@ -26,16 +23,13 @@ function operation (operation) {
|
||||
} else {
|
||||
const vesselOrgs = new Organisations(await vesselOrganisations());
|
||||
const availableOrgs = vesselOrgs.accessToOperation(operation).filter(user.organisations);
|
||||
console.log("Operation: ", operation);
|
||||
console.log("User: ", user.name);
|
||||
console.log("User orgs: ", user.organisations);
|
||||
console.log("Vessel orgs: ", vesselOrgs);
|
||||
console.log("Available orgs: ", availableOrgs);
|
||||
DEBUG(`operation = ${operation}, user = ${user?.name}, user orgs = %j, vessel orgs = %j, availableOrgs = %j`, user.organisations.toJSON(), vesselOrgs.toJSON(), availableOrgs.toJSON());
|
||||
if (availableOrgs.length > 0) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
DEBUG(`Access denied to operation ${operation}.`);
|
||||
next({status: 403, message: "Access denied"});
|
||||
}
|
||||
}
|
||||
@@ -67,7 +61,7 @@ async function user (req, res, next) {
|
||||
async function admin (req, res, next) {
|
||||
if (req.user) {
|
||||
const user = new ServerUser(req.user);
|
||||
if (user.operations.accessToOperation("edit").length > 0) {
|
||||
if (user.organisations.accessToOperation("edit").length > 0) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,123 @@
|
||||
const dns = require('dns');
|
||||
const { Netmask } = require('netmask');
|
||||
const ipaddr = require('ipaddr.js');
|
||||
const { isIPv6, isIPv4 } = require('net');
|
||||
const cfg = require('../../../lib/config');
|
||||
const jwt = require('../../../lib/jwt');
|
||||
const user = require('../../../lib/db/user');
|
||||
const ServerUser = require('../../../lib/db/user/User');
|
||||
const { ERROR, WARNING, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
|
||||
|
||||
async function authorisedIP (req, res) {
|
||||
const validIPs = await user.ip({active: true}); // Get all active IP logins
|
||||
validIPs.forEach( i => i.$block = new Netmask(i.ip) );
|
||||
validIPs.sort( (a, b) => b.$block.bitmask - a.$block.netmask ); // More specific IPs have precedence
|
||||
for (const ip of validIPs) {
|
||||
const block = ip.$block;
|
||||
if (block.contains(req.ip)) {
|
||||
const payload = {
|
||||
...ip,
|
||||
ip: req.ip,
|
||||
autologin: true
|
||||
};
|
||||
delete payload.$block;
|
||||
delete payload.hash;
|
||||
delete payload.active;
|
||||
jwt.issue(payload, req, res);
|
||||
return true;
|
||||
function parseIP(ip) {
|
||||
if (!ip || typeof ip !== 'string') {
|
||||
WARNING('Invalid IP input:', ip);
|
||||
return null;
|
||||
}
|
||||
// Handle comma-separated X-Forwarded-For (e.g., "87.90.254.127,")
|
||||
const cleanIp = ip.split(',')[0].trim();
|
||||
if (!cleanIp) {
|
||||
WARNING('Empty IP after parsing:', ip);
|
||||
return null;
|
||||
}
|
||||
// Convert IPv6-mapped IPv4 (e.g., ::ffff:127.0.0.1 -> 127.0.0.1)
|
||||
if (cleanIp.startsWith('::ffff:') && isIPv4(cleanIp.split('::ffff:')[1])) {
|
||||
return cleanIp.split('::ffff:')[1];
|
||||
}
|
||||
return cleanIp;
|
||||
}
|
||||
|
||||
function normalizeCIDR(range) {
|
||||
if (!range || typeof range !== 'string') {
|
||||
WARNING('Invalid CIDR range:', range);
|
||||
return null;
|
||||
}
|
||||
// If no /prefix, assume /32 for IPv4 or /128 for IPv6
|
||||
if (!range.includes('/')) {
|
||||
try {
|
||||
const parsed = ipaddr.parse(range);
|
||||
const prefix = parsed.kind() === 'ipv4' ? 32 : 128;
|
||||
return `${range}/${prefix}`;
|
||||
} catch (err) {
|
||||
WARNING(`Failed to parse bare IP ${range}:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
async function authorisedIP(req, res) {
|
||||
const ip = parseIP(req.ip || req.headers['x-forwarded-for'] || req.headers['x-real-ip']);
|
||||
DEBUG('authorisedIP:', { ip, headers: req.headers }); // Debug
|
||||
if (!ip) {
|
||||
WARNING('No valid IP provided:', { ip, headers: req.headers });
|
||||
return false;
|
||||
}
|
||||
|
||||
let addr;
|
||||
try {
|
||||
addr = ipaddr.parse(ip);
|
||||
} catch (err) {
|
||||
WARNING('Invalid IP:', ip, err.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
const validIPs = await user.ip({ active: true }); // Get active IP logins
|
||||
// Attach parsed CIDR to each IP entry
|
||||
validIPs.forEach(i => {
|
||||
const normalized = normalizeCIDR(i.ip);
|
||||
if (!normalized) {
|
||||
i.$range = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const [rangeAddr, prefix] = ipaddr.parseCIDR(normalized);
|
||||
i.$range = { addr: rangeAddr, prefix };
|
||||
} catch (err) {
|
||||
WARNING(`Invalid CIDR range ${i.ip}:`, err.message);
|
||||
i.$range = null; // Skip invalid ranges
|
||||
}
|
||||
});
|
||||
// Filter out invalid ranges and sort by specificity (descending prefix length)
|
||||
const validRanges = validIPs.filter(i => i.$range).sort((a, b) => b.$range.prefix - a.$range.prefix);
|
||||
|
||||
for (const ipEntry of validRanges) {
|
||||
const { addr: rangeAddr, prefix } = ipEntry.$range;
|
||||
try {
|
||||
if (addr.match(rangeAddr, prefix)) {
|
||||
const payload = {
|
||||
...ipEntry,
|
||||
ip,
|
||||
autologin: true
|
||||
};
|
||||
delete payload.$range;
|
||||
delete payload.hash;
|
||||
delete payload.active;
|
||||
jwt.issue(payload, req, res);
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
WARNING(`Error checking range ${ipEntry.ip}:`, err.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function authorisedHost (req, res) {
|
||||
const validHosts = await user.host({active: true}); // Get all active host logins
|
||||
async function authorisedHost(req, res) {
|
||||
const ip = parseIP(req.ip || req.headers['x-forwarded-for'] || req.headers['x-real-ip']);
|
||||
DEBUG('authorisedHost:', { ip, headers: req.headers }); // Debug
|
||||
if (!ip) {
|
||||
WARNING('No valid IP for host check:', { ip, headers: req.headers });
|
||||
return false;
|
||||
}
|
||||
|
||||
const validHosts = await user.host({ active: true });
|
||||
for (const key in validHosts) {
|
||||
try {
|
||||
const ip = await dns.promises.resolve(key);
|
||||
if (ip == req.ip) {
|
||||
const resolvedIPs = await dns.promises.resolve(key);
|
||||
if (resolvedIPs.includes(ip)) {
|
||||
const payload = {
|
||||
...validHosts[key],
|
||||
ip: req.ip,
|
||||
ip,
|
||||
autologin: true
|
||||
};
|
||||
delete payload.$block;
|
||||
@@ -45,49 +127,28 @@ async function authorisedHost (req, res) {
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code != "ENODATA") {
|
||||
console.error(err);
|
||||
if (err.code !== 'ENODATA') {
|
||||
ERROR(`DNS error for host ${key}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Check client TLS certificates
|
||||
// Probably will do this via Nginx with
|
||||
// ssl_verify_client optional;
|
||||
// and then putting either of the
|
||||
// $ssl_client_s_dn or $ssl_client_escaped_cert
|
||||
// variables into an HTTP header for Node
|
||||
// to check (naturally, it must be ensured
|
||||
// that a user cannot just insert the header
|
||||
// in a request).
|
||||
|
||||
|
||||
async function auth (req, res, next) {
|
||||
|
||||
async function auth(req, res, next) {
|
||||
if (res.headersSent) {
|
||||
// Nothing to do, this request must have been
|
||||
// handled already by another middleware.
|
||||
return;
|
||||
return; // Handled by another middleware
|
||||
}
|
||||
|
||||
// Check for a valid JWT (already decoded by a previous
|
||||
// middleware).
|
||||
// Check for valid JWT
|
||||
if (req.user) {
|
||||
if (!req.user.autologin) {
|
||||
// If this is not an automatic login, check if the token is in the
|
||||
// second half of its lifetime. If so, reissue a new one, valid for
|
||||
// another cfg.jwt.options.expiresIn seconds.
|
||||
if (req.user.exp) {
|
||||
const ttl = req.user.exp - Date.now()/1000;
|
||||
if (ttl < cfg.jwt.options.expiresIn/2) {
|
||||
const credentials = await ServerUser.fromSQL(null, req.user.id);
|
||||
if (credentials) {
|
||||
// Refresh token
|
||||
payload = Object.assign({}, credentials.toJSON());
|
||||
jwt.issue(Object.assign({}, credentials.toJSON()), req, res);
|
||||
}
|
||||
if (!req.user.autologin && req.user.exp) {
|
||||
const ttl = req.user.exp - Date.now() / 1000;
|
||||
if (ttl < cfg.jwt.options.expiresIn / 2) {
|
||||
const credentials = await ServerUser.fromSQL(null, req.user.id);
|
||||
if (credentials) {
|
||||
const payload = Object.assign({}, credentials.toJSON());
|
||||
jwt.issue(payload, req, res);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,19 +156,27 @@ async function auth (req, res, next) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP is known to us
|
||||
// Check IP and host
|
||||
if (await authorisedIP(req, res)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the hostname is known to us
|
||||
if (await authorisedHost(req, res)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
next({status: 401, message: "Not authorised"});
|
||||
// If *all* else fails, check if the user came with a cookie
|
||||
// (see https://gitlab.com/wgp/dougal/software/-/issues/335)
|
||||
if (req.cookies.JWT) {
|
||||
const token = req.cookies.JWT;
|
||||
delete req.cookies.JWT;
|
||||
DEBUG("falling back to cookie-based authentication");
|
||||
req.user = await jwt.checkValidCredentials({jwt: token});
|
||||
return await auth(req, res, next);
|
||||
}
|
||||
|
||||
next({ status: 401, message: 'Not authorised' });
|
||||
}
|
||||
|
||||
module.exports = auth;
|
||||
|
||||
@@ -5,18 +5,31 @@ const cfg = require("../../../lib/config").jwt;
|
||||
const getToken = function (req) {
|
||||
if (req.headers.authorization && req.headers.authorization.split(' ')[0] == 'Bearer') {
|
||||
return req.headers.authorization.split(' ')[1];
|
||||
} else if (req.cookies.JWT) {
|
||||
return req.cookies.JWT;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const onExpired = async function (req, err) {
|
||||
// If it's not too badly expired, let it through
|
||||
// and hope that a new token will be issued soon.
|
||||
const elapsed = new Date() - err.inner.expiredAt;
|
||||
// TODO: Add proper logging
|
||||
// console.log("Expiry details (elapsed, gracePeriod)", elapsed, cfg.gracePeriod*1000);
|
||||
if (elapsed < cfg.gracePeriod*1000) {
|
||||
// console.log("JWT within grace period");
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const options = {
|
||||
secret: cfg.secret,
|
||||
credentialsRequired: false,
|
||||
algorithms: ['HS256'],
|
||||
requestProperty: "user",
|
||||
getToken
|
||||
getToken,
|
||||
onExpired
|
||||
};
|
||||
|
||||
module.exports = expressJWT(options);
|
||||
|
||||
18
lib/www/server/api/middleware/compress/index.js
Normal file
18
lib/www/server/api/middleware/compress/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const compression = require('compression');
|
||||
|
||||
const compress = compression({
|
||||
level: 6, // Balance speed vs. ratio (1-9)
|
||||
threshold: 512, // Compress only if response >512 bytes to avoid overhead on small bundles
|
||||
filter: (req, res) => { // Ensure bundles are compressed
|
||||
const accept = req.get("Accept");
|
||||
if (accept.startsWith("application/vnd.aaltronav.dougal+octet-stream")) return true;
|
||||
if (accept.includes("json")) return true;
|
||||
if (accept.startsWith("text/")) return true;
|
||||
if (accept.startsWith("model/obj")) return true;
|
||||
|
||||
// fallback to standard filter function
|
||||
return compression.filter(req, res)
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = compress;
|
||||
@@ -23,9 +23,9 @@ function ifNoneMatch (req, res, next) {
|
||||
if (cached) {
|
||||
DEBUG("ETag match. Returning cached response (ETag: %s, If-None-Match: %s) for %s %s",
|
||||
cached.etag, req.get("If-None-Match"), req.method, req.url);
|
||||
setHeaders(res, cached.headers);
|
||||
if (req.method == "GET" || req.method == "HEAD") {
|
||||
res.status(304).send();
|
||||
setHeaders(res, cached.headers);
|
||||
res.status(304).end();
|
||||
// No next()
|
||||
} else if (!isIdempotentMethod(req.method)) {
|
||||
res.status(412).send();
|
||||
|
||||
@@ -66,8 +66,18 @@ const rels = [
|
||||
|
||||
function invalidateCache (data, cache) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!data) {
|
||||
ERROR("invalidateCache called with no data");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.payload) {
|
||||
ERROR("invalidateCache called without a payload; channel = %s", data.channel);
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = data.channel;
|
||||
const project = data.payload.pid ?? data.payload?.new?.pid ?? data.payload?.old?.pid;
|
||||
const project = data.payload?.pid ?? data.payload?.new?.pid ?? data.payload?.old?.pid;
|
||||
const operation = data.payload.operation;
|
||||
const table = data.payload.table;
|
||||
const fields = { channel, project, operation, table };
|
||||
|
||||
146
lib/www/server/api/middleware/event/import/csv.js
Normal file
146
lib/www/server/api/middleware/event/import/csv.js
Normal file
@@ -0,0 +1,146 @@
|
||||
const Busboy = require('busboy');
|
||||
const { parse } = require('csv-parse/sync');
|
||||
|
||||
async function middleware(req, res, next) {
|
||||
const contentType = req.headers['content-type'] || '';
|
||||
let csvText = null;
|
||||
let filename = null;
|
||||
|
||||
if (req.params.filename && contentType.startsWith('text/csv')) {
|
||||
csvText = typeof req.body === 'string' ? req.body : req.body.toString('utf8');
|
||||
filename = req.params.filename;
|
||||
processCsv();
|
||||
} else if (contentType.startsWith('multipart/form-data')) {
|
||||
const busboy = Busboy({ headers: req.headers });
|
||||
let found = false;
|
||||
busboy.on('file', (name, file, info) => {
|
||||
if (found) {
|
||||
file.resume();
|
||||
return;
|
||||
}
|
||||
if (info.mimeType === 'text/csv') {
|
||||
found = true;
|
||||
filename = info.filename || 'unnamed.csv';
|
||||
csvText = '';
|
||||
file.setEncoding('utf8');
|
||||
file.on('data', (data) => { csvText += data; });
|
||||
file.on('end', () => {});
|
||||
} else {
|
||||
file.resume();
|
||||
}
|
||||
});
|
||||
busboy.on('field', () => {}); // Ignore fields
|
||||
busboy.on('finish', () => {
|
||||
if (!found) {
|
||||
return next();
|
||||
}
|
||||
processCsv();
|
||||
});
|
||||
req.pipe(busboy);
|
||||
return;
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
|
||||
function processCsv() {
|
||||
let records;
|
||||
try {
|
||||
records = parse(csvText, {
|
||||
relax_quotes: true,
|
||||
quote: '"',
|
||||
escape: '"',
|
||||
skip_empty_lines: true,
|
||||
trim: true
|
||||
});
|
||||
} catch (e) {
|
||||
return res.status(400).json({ error: 'Invalid CSV' });
|
||||
}
|
||||
if (!records.length) {
|
||||
return res.status(400).json({ error: 'Empty CSV' });
|
||||
}
|
||||
const headers = records[0].map(h => h.toLowerCase().trim());
|
||||
const rows = records.slice(1);
|
||||
let lastDate = null;
|
||||
let lastTime = null;
|
||||
const currentDate = new Date().toISOString().slice(0, 10);
|
||||
const currentTime = new Date().toISOString().slice(11, 19);
|
||||
const events = [];
|
||||
for (let row of rows) {
|
||||
let object = { labels: [] };
|
||||
for (let k = 0; k < headers.length; k++) {
|
||||
let key = headers[k];
|
||||
let val = row[k] ? row[k].trim() : '';
|
||||
if (!key) continue;
|
||||
if (['remarks', 'event', 'comment', 'comments', 'text'].includes(key)) {
|
||||
object.remarks = val;
|
||||
} else if (key === 'label') {
|
||||
if (val) object.labels.push(val);
|
||||
} else if (key === 'labels') {
|
||||
if (val) object.labels.push(...val.split(';').map(l => l.trim()).filter(l => l));
|
||||
} else if (key === 'sequence' || key === 'seq') {
|
||||
if (val) object.sequence = Number(val);
|
||||
} else if (['point', 'shot', 'shotpoint'].includes(key)) {
|
||||
if (val) object.point = Number(val);
|
||||
} else if (key === 'date') {
|
||||
object.date = val;
|
||||
} else if (key === 'time') {
|
||||
object.time = val;
|
||||
} else if (key === 'timestamp') {
|
||||
object.timestamp = val;
|
||||
} else if (key === 'latitude') {
|
||||
object.latitude = parseFloat(val);
|
||||
} else if (key === 'longitude') {
|
||||
object.longitude = parseFloat(val);
|
||||
}
|
||||
}
|
||||
if (!object.remarks) continue;
|
||||
let useSeqPoint = Number.isFinite(object.sequence) && Number.isFinite(object.point);
|
||||
let tstamp = null;
|
||||
if (!useSeqPoint) {
|
||||
if (object.timestamp) {
|
||||
tstamp = new Date(object.timestamp);
|
||||
}
|
||||
if (!tstamp || isNaN(tstamp.getTime())) {
|
||||
let dateStr = object.date || lastDate || currentDate;
|
||||
let timeStr = object.time || lastTime || currentTime;
|
||||
if (timeStr.length === 5) timeStr += ':00';
|
||||
let full = `${dateStr}T${timeStr}.000Z`;
|
||||
tstamp = new Date(full);
|
||||
if (isNaN(tstamp.getTime())) continue;
|
||||
}
|
||||
if (object.date) lastDate = object.date;
|
||||
if (object.time) lastTime = object.time;
|
||||
}
|
||||
let event = {
|
||||
remarks: object.remarks,
|
||||
labels: object.labels,
|
||||
meta: {
|
||||
author: "*CSVImport*",
|
||||
"*CSVImport*": {
|
||||
filename,
|
||||
tstamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!isNaN(object.latitude) && !isNaN(object.longitude)) {
|
||||
event.meta.geometry = {
|
||||
type: "Point",
|
||||
coordinates: [object.longitude, object.latitude]
|
||||
};
|
||||
}
|
||||
if (useSeqPoint) {
|
||||
event.sequence = object.sequence;
|
||||
event.point = object.point;
|
||||
} else if (tstamp) {
|
||||
event.tstamp = tstamp.toISOString();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
events.push(event);
|
||||
}
|
||||
req.body = events;
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = middleware;
|
||||
18
lib/www/server/api/middleware/event/import/delete.js
Normal file
18
lib/www/server/api/middleware/event/import/delete.js
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
const { event } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
if (req.params.project && req.params.filename) {
|
||||
await event.unimport(req.params.project, req.params.filename, req.query);
|
||||
res.status(204).end();
|
||||
} else {
|
||||
res.status(400).send({message: "Malformed request"});
|
||||
}
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
};
|
||||
6
lib/www/server/api/middleware/event/import/index.js
Normal file
6
lib/www/server/api/middleware/event/import/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
module.exports = {
|
||||
csv: require('./csv'),
|
||||
put: require('./put'),
|
||||
delete: require('./delete'),
|
||||
}
|
||||
16
lib/www/server/api/middleware/event/import/put.js
Normal file
16
lib/www/server/api/middleware/event/import/put.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
const { event } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
const payload = req.body;
|
||||
|
||||
await event.import(req.params.project, payload, req.query);
|
||||
res.status(200).send(payload);
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -7,5 +7,6 @@ module.exports = {
|
||||
put: require('./put'),
|
||||
patch: require('./patch'),
|
||||
delete: require('./delete'),
|
||||
changes: require('./changes')
|
||||
changes: require('./changes'),
|
||||
import: require('./import'),
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const { gis } = require('../../../../lib/db');
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
res.set("Content-Type", "application/geo+json");
|
||||
res.status(200).send(await gis.project.bbox(req.params.project));
|
||||
next();
|
||||
} catch (err) {
|
||||
|
||||
@@ -11,6 +11,7 @@ module.exports = {
|
||||
gis: require('./gis'),
|
||||
label: require('./label'),
|
||||
navdata: require('./navdata'),
|
||||
vessel: require('./vessel'),
|
||||
queue: require('./queue'),
|
||||
qc: require('./qc'),
|
||||
configuration: require('./configuration'),
|
||||
@@ -20,5 +21,6 @@ module.exports = {
|
||||
rss: require('./rss'),
|
||||
etag: require('./etag'),
|
||||
version: require('./version'),
|
||||
admin: require('./admin')
|
||||
admin: require('./admin'),
|
||||
compress: require('./compress'),
|
||||
};
|
||||
|
||||
17
lib/www/server/api/middleware/line/get/binary.js
Normal file
17
lib/www/server/api/middleware/line/get/binary.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { bundle } = require('../../../../lib/binary');
|
||||
const { line } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
const json = await line.get(req.params.project, req.params.class, req.params.line, {wgs84: true, ...req.query});
|
||||
const data = bundle(json, {type: req.params.class == "source" ? 1 : 0});
|
||||
console.log("bundle", data);
|
||||
|
||||
res.status(200).send(Buffer.from(data));
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
};
|
||||
27
lib/www/server/api/middleware/line/get/geojson.js
Normal file
27
lib/www/server/api/middleware/line/get/geojson.js
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
const { line } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
const json = await line.get(req.params.project, req.params.class, req.params.line, {wgs84: true, ...req.query});
|
||||
|
||||
const geojson = {
|
||||
type: "FeatureCollection",
|
||||
features: json.map(feature => {
|
||||
return {
|
||||
type: "Feature",
|
||||
geometry: feature["geometry"],
|
||||
properties: {...feature}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
res.status(200).send(geojson);
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
26
lib/www/server/api/middleware/line/get/index.js
Normal file
26
lib/www/server/api/middleware/line/get/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const json = require('./json');
|
||||
const geojson = require('./geojson');
|
||||
const binary = require('./binary');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
try {
|
||||
const handlers = {
|
||||
"application/json": json,
|
||||
"application/geo+json": geojson,
|
||||
"application/vnd.aaltronav.dougal+octet-stream": binary,
|
||||
"application/vnd.aaltronav.dougal+octet-stream; format=0x1c": binary,
|
||||
};
|
||||
|
||||
const mimetype = (handlers[req.query.mime] && req.query.mime) || req.accepts(Object.keys(handlers));
|
||||
|
||||
if (mimetype) {
|
||||
res.set("Content-Type", mimetype);
|
||||
await handlers[mimetype](req, res, next);
|
||||
} else {
|
||||
res.status(406).send();
|
||||
next();
|
||||
}
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
14
lib/www/server/api/middleware/line/get/json.js
Normal file
14
lib/www/server/api/middleware/line/get/json.js
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
const { line } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
res.status(200).send(await line.get(req.params.project, req.params.class, req.params.line, req.query));
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
17
lib/www/server/api/middleware/line/list/binary.js
Normal file
17
lib/www/server/api/middleware/line/list/binary.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { bundle } = require('../../../../lib/binary');
|
||||
const { line } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
const json = await line.get(req.params.project, req.params.class, req.params.line, {wgs84: true, ...req.query});
|
||||
const data = bundle(json, {type: req.query.type ?? 0});
|
||||
console.log("bundle", data);
|
||||
|
||||
res.status(200).send(Buffer.from(data));
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
};
|
||||
27
lib/www/server/api/middleware/line/list/geojson.js
Normal file
27
lib/www/server/api/middleware/line/list/geojson.js
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
const { line } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
const json = await line.get(req.params.project, req.params.class, req.params.line, {wgs84: true, ...req.query});
|
||||
|
||||
const geojson = {
|
||||
type: "FeatureCollection",
|
||||
features: json.map(feature => {
|
||||
return {
|
||||
type: "Feature",
|
||||
geometry: feature["geometry"],
|
||||
properties: {...feature}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
res.status(200).send(geojson);
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
26
lib/www/server/api/middleware/line/list/index.js
Normal file
26
lib/www/server/api/middleware/line/list/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const json = require('./json');
|
||||
const geojson = require('./geojson');
|
||||
const binary = require('./binary');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
try {
|
||||
const handlers = {
|
||||
"application/json": json,
|
||||
"application/geo+json": geojson,
|
||||
"application/vnd.aaltronav.dougal+octet-stream": binary,
|
||||
"application/vnd.aaltronav.dougal+octet-stream; format=0x1c": binary,
|
||||
};
|
||||
|
||||
const mimetype = (handlers[req.query.mime] && req.query.mime) || req.accepts(Object.keys(handlers));
|
||||
|
||||
if (mimetype) {
|
||||
res.set("Content-Type", mimetype);
|
||||
await handlers[mimetype](req, res, next);
|
||||
} else {
|
||||
res.status(406).send();
|
||||
next();
|
||||
}
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
const { line } = require('../../../lib/db');
|
||||
const { line } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
@@ -10,4 +10,5 @@ module.exports = async function (req, res, next) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -10,7 +10,6 @@ function json (req, res, next) {
|
||||
} else {
|
||||
res.status(404).send({message: "Not found"});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function yaml (req, res, next) {
|
||||
@@ -19,7 +18,6 @@ function yaml (req, res, next) {
|
||||
} else {
|
||||
res.status(404).send({message: "Not found"});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function csv (req, res, next) {
|
||||
@@ -33,7 +31,6 @@ function csv (req, res, next) {
|
||||
} else {
|
||||
res.status(404).send({message: "Not found"});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
@@ -53,9 +50,10 @@ module.exports = async function (req, res, next) {
|
||||
await handlers[mimetype](req, res, next);
|
||||
} else {
|
||||
res.status(406).send();
|
||||
next();
|
||||
}
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
30
lib/www/server/api/middleware/sequence/get/binary.js
Normal file
30
lib/www/server/api/middleware/sequence/get/binary.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const { bundle } = require('../../../../lib/binary');
|
||||
const { sequence } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
let json = await sequence.get(req.params.project, req.params.sequence, req.query);
|
||||
const type = req.query.type ?? 2;
|
||||
if (type == 2) {
|
||||
// Filter out missing raw data
|
||||
json = json.filter(el => el.geometryraw);
|
||||
} else if (type == 3) {
|
||||
// Filter out missing final data
|
||||
json = json.filter(el => el.geometryfinal);
|
||||
}
|
||||
|
||||
if (json.length) {
|
||||
const data = bundle(json, {type});
|
||||
console.log("bundle", data);
|
||||
res.status(200).send(Buffer.from(data));
|
||||
} else {
|
||||
res.status(404).send();
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
const json = require('./json');
|
||||
const geojson = require('./geojson');
|
||||
const binary = require('./binary');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
try {
|
||||
const handlers = {
|
||||
"application/json": json,
|
||||
"application/geo+json": geojson,
|
||||
"application/vnd.aaltronav.dougal+octet-stream": binary,
|
||||
"application/vnd.aaltronav.dougal+octet-stream; format=0x1c": binary,
|
||||
};
|
||||
|
||||
const mimetype = (handlers[req.query.mime] && req.query.mime) || req.accepts(Object.keys(handlers));
|
||||
|
||||
17
lib/www/server/api/middleware/sequence/list/binary.js
Normal file
17
lib/www/server/api/middleware/sequence/list/binary.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { bundle } = require('../../../../lib/binary');
|
||||
const { sequence } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
const json = await sequence.get(req.params.project, null, req.query);
|
||||
const data = bundle(json, {type: req.query.type ?? 2});
|
||||
console.log("bundle", data);
|
||||
|
||||
res.status(200).send(Buffer.from(data));
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
};
|
||||
33
lib/www/server/api/middleware/sequence/list/geojson.js
Normal file
33
lib/www/server/api/middleware/sequence/list/geojson.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const { ERROR, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
|
||||
|
||||
const { sequence } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
const ts0 = Date.now();
|
||||
const json = await sequence.get(req.params.project, null, req.query);
|
||||
const geometry = req.query.geometry || "geometrypreplot";
|
||||
const δt = Date.now()-ts0;
|
||||
DEBUG("Full-project data retrieval completed in %.3f s", δt / 1000);
|
||||
|
||||
|
||||
const geojson = {
|
||||
type: "FeatureCollection",
|
||||
features: json.map(feature => {
|
||||
return {
|
||||
type: "Feature",
|
||||
geometry: feature[geometry],
|
||||
properties: {...feature}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
res.status(200).send(geojson);
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
26
lib/www/server/api/middleware/sequence/list/index.js
Normal file
26
lib/www/server/api/middleware/sequence/list/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const json = require('./json');
|
||||
const geojson = require('./geojson');
|
||||
const binary = require('./binary');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
try {
|
||||
const handlers = {
|
||||
"application/json": json,
|
||||
"application/geo+json": geojson,
|
||||
"application/vnd.aaltronav.dougal+octet-stream": binary,
|
||||
"application/vnd.aaltronav.dougal+octet-stream; format=0x1c": binary,
|
||||
};
|
||||
|
||||
const mimetype = (handlers[req.query.mime] && req.query.mime) || req.accepts(Object.keys(handlers));
|
||||
|
||||
if (mimetype) {
|
||||
res.set("Content-Type", mimetype);
|
||||
await handlers[mimetype](req, res, next);
|
||||
} else {
|
||||
res.status(406).send();
|
||||
next();
|
||||
}
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
|
||||
const { sequence } = require('../../../lib/db');
|
||||
const { sequence } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
console.log("JSON");
|
||||
res.status(200).send(await sequence.list(req.params.project, req.query));
|
||||
next();
|
||||
} catch (err) {
|
||||
@@ -6,8 +6,9 @@ async function login (req, res, next) {
|
||||
const {user, password} = req.body;
|
||||
const payload = await jwt.checkValidCredentials({user, password});
|
||||
if (payload) {
|
||||
jwt.issue(payload, req, res);
|
||||
res.status(204).send();
|
||||
const token = jwt.issue(payload, req, res);
|
||||
res.set("X-JWT", token);
|
||||
res.status(200).send({token});
|
||||
next();
|
||||
return;
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
async function logout (req, res, next) {
|
||||
res.clearCookie("JWT");
|
||||
res.status(204).send();
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ module.exports = async function (req, res, next) {
|
||||
|
||||
if (requestor.id == target.id) {
|
||||
// User cannot self-deactivate
|
||||
newUser.active = requestor.active;
|
||||
target.active = requestor.active;
|
||||
}
|
||||
|
||||
const edited = await requestor.edit(target).to(changes).save();
|
||||
|
||||
15
lib/www/server/api/middleware/version/history/get.js
Normal file
15
lib/www/server/api/middleware/version/history/get.js
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
const version = require('../../../../lib/version');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
const v = await version.history(req.query.count, req.query.lines);
|
||||
res.status(200).json(v);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
|
||||
};
|
||||
4
lib/www/server/api/middleware/version/history/index.js
Normal file
4
lib/www/server/api/middleware/version/history/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get')
|
||||
get: require('./get'),
|
||||
history: require('./history'),
|
||||
}
|
||||
|
||||
4
lib/www/server/api/middleware/vessel/index.js
Normal file
4
lib/www/server/api/middleware/vessel/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
module.exports = {
|
||||
track: require('./track'),
|
||||
}
|
||||
10
lib/www/server/api/middleware/vessel/track/get.js
Normal file
10
lib/www/server/api/middleware/vessel/track/get.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const { gis } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
try {
|
||||
res.status(200).send(await gis.vesseltrack.get(req.query));
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user