Also serve preplot source/sail points as binary.

This commit adds the ability to pack preplot points in Dougal
binary format. Sail line points take udv=0 and source line points
take udv=1 – udv=2 remains sequence data.

Endpoints for retrieving the data in JSON, GeoJSON and binary
formats have also been added. Data may be retrieved as a single
line or for a whole project.
This commit is contained in:
D. Berge
2025-08-03 11:17:31 +02:00
parent 2bcdee03d5
commit c376896ea6
16 changed files with 359 additions and 26 deletions

View File

@@ -155,10 +155,13 @@ app.map({
'/project/:project/line/': { '/project/:project/line/': {
get: [ mw.auth.access.read, mw.line.list ], get: [ mw.auth.access.read, mw.line.list ],
}, },
'/project/:project/line/:line': { '/project/:project/line/:line(\\d+)': {
// get: [ mw.auth.access.read, mw.line.get ], get: [ mw.auth.access.read, mw.line.get ],
patch: [ mw.auth.access.write, mw.line.patch ], patch: [ mw.auth.access.write, mw.line.patch ],
}, },
'/project/:project/line/:class(sail|source)': {
get: [ mw.auth.access.read, mw.line.get ],
},
/* /*
* Sequence endpoints * Sequence endpoints

View 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);
}
};

View 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);
}
};

View 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);
}
}

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
}

View File

@@ -1,5 +1,5 @@
const { line } = require('../../../lib/db'); const { line } = require('../../../../lib/db');
module.exports = async function (req, res, next) { module.exports = async function (req, res, next) {
@@ -10,4 +10,5 @@ module.exports = async function (req, res, next) {
next(err); next(err);
} }
}; };

View File

@@ -5,7 +5,7 @@ module.exports = async function (req, res, next) {
try { try {
const json = await sequence.get(req.params.project, req.params.sequence, req.query); const json = await sequence.get(req.params.project, req.params.sequence, req.query);
const data = bundle(json, {type: req.query.type}); const data = bundle(json, {type: req.query.type ?? 2});
console.log("bundle", data); console.log("bundle", data);
res.status(200).send(Buffer.from(data)); res.status(200).send(Buffer.from(data));

View File

@@ -7,8 +7,8 @@ module.exports = async function (req, res, next) {
const handlers = { const handlers = {
"application/json": json, "application/json": json,
"application/geo+json": geojson, "application/geo+json": geojson,
// application/vnd.aaltronav.dougal+octet-stream; format=0x1c "application/vnd.aaltronav.dougal+octet-stream": binary,
"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)); const mimetype = (handlers[req.query.mime] && req.query.mime) || req.accepts(Object.keys(handlers));

View File

@@ -5,7 +5,7 @@ module.exports = async function (req, res, next) {
try { try {
const json = await sequence.get(req.params.project, null, req.query); const json = await sequence.get(req.params.project, null, req.query);
const data = bundle(json, {type: req.query.type}); const data = bundle(json, {type: req.query.type ?? 2});
console.log("bundle", data); console.log("bundle", data);
res.status(200).send(Buffer.from(data)); res.status(200).send(Buffer.from(data));

View File

@@ -7,7 +7,8 @@ module.exports = async function (req, res, next) {
const handlers = { const handlers = {
"application/json": json, "application/json": json,
"application/geo+json": geojson, "application/geo+json": geojson,
"application/vnd.aaltronav.dougal+octet-stream": binary "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)); const mimetype = (handlers[req.query.mime] && req.query.mime) || req.accepts(Object.keys(handlers));

View File

@@ -11,6 +11,73 @@ function bundle (json, opts = {}) {
// console.log("JSON LENGTH", json.length); // console.log("JSON LENGTH", json.length);
// console.log("OPTS", geometries, payload); // console.log("OPTS", geometries, payload);
if (type == 0) {
/* Preplot information sail line points
*
* elem 0: Float32Array Longitude
* elem 1: Float32Array Latitude
* elem 2: Uint8Array Flags: 0x01 point ntba, 0x02 sailline ntba
*/
// Add preplot positions
values.push({
// longitude
key: el => el.geometry.coordinates?.[0],
type: Float32Array
});
values.push({
// latitude
key: el => el.geometry?.coordinates?.[1],
type: Float32Array
});
values.push({
// flags
key: el => (el.sailline_ntba ? 0x02 : 0) | (el.ntba ? 0x01 : 0),
type: Uint8Array
});
return encode.sequential(json, el => el.sailline, el => el.point, deltas, values, type)
} if (type == 1) {
/* Preplot information source line points
*
* elem 0: Float32Array Longitude
* elem 1: Float32Array Latitude
* elem 2: Uint8Array Flags: 0x01 point ntba, 0x02 sailline ntba
* elem 3: Uint16Array Sailline
*/
// Add preplot positions
values.push({
// longitude
key: el => el.geometry.coordinates?.[0],
type: Float32Array
});
values.push({
// latitude
key: el => el.geometry?.coordinates?.[1],
type: Float32Array
});
values.push({
// flags
key: el => (el.sailline_ntba ? 0x02 : 0) | (el.ntba ? 0x01 : 0),
type: Uint8Array
});
values.push({
// sailline
key: el => el.sailline,
type: Uint16Array
});
console.log("JSON", json[0]);
return encode.sequential(json, el => el.line, el => el.point, deltas, values, type)
} else if (type == 2) {
/* Gun information: /* Gun information:
* *
* - Δelem 0: BigUint64Array + Int16Array timestamps * - Δelem 0: BigUint64Array + Int16Array timestamps
@@ -23,7 +90,6 @@ function bundle (json, opts = {}) {
* - elem 1618: Gun delay (μ, σ, R) * - elem 1618: Gun delay (μ, σ, R)
* - elem 19: No fire / autofire (in a single byte) * - elem 19: No fire / autofire (in a single byte)
*/ */
if (type == 2) {
// Add timestamps // Add timestamps
deltas.push({ deltas.push({
@@ -155,12 +221,12 @@ function bundle (json, opts = {}) {
key: el => (el.meta.raw?.smsrc?.no_fire ?? 0) << 4 | (el.meta.raw?.smsrc?.num_auto ?? 0), key: el => (el.meta.raw?.smsrc?.no_fire ?? 0) << 4 | (el.meta.raw?.smsrc?.num_auto ?? 0),
type: Uint8Array type: Uint8Array
}); });
}
console.log("DELTAS", deltas); console.log("DELTAS", deltas);
console.log("VALUES", values); console.log("VALUES", values);
return encode.sequential(json, el => el.sequence, el => el.point, deltas, values, type) return encode.sequential(json, el => el.sequence, el => el.point, deltas, values, type)
} }
}
module.exports = { bundle }; module.exports = { bundle };

View File

@@ -0,0 +1,108 @@
const { setSurvey } = require('../connection');
async function getSummary (projectId, line, opts = {}) {
const client = await setSurvey(projectId);
const text = `
SELECT pls.line, pls.fsp, pls.lsp, pls.num_points, pls.length, pls.azimuth, pls.incr,
pl.ntba, pl.geometry, pl.remarks, pl.meta
FROM preplot_lines_summary pls
INNER JOIN preplot_lines pl
ON psl.line = pl.line and pl.class = 'V'
WHERE ($1:integer IS NULL) OR (line = $1);
`;
const res = await client.query(text, [line]);
client.release();
return res.rows[0];
}
async function get (projectId, type = "sail", line = null, opts = {}) {
if (opts.summary) {
return await getSummary(projectId, line, opts);
}
const client = await setSurvey(projectId);
const sortFields = [
"sailline", "line", "point", "ntba", "sailline_ntba"
];
const sortKey = opts.sortBy && sortFields.includes(opts.sortBy) && opts.sortBy || "line";
const sortDir = opts.sortDesc == "false" ? "ASC" : "DESC";
const offset = Math.abs(opts.offset) || Math.abs((opts.page-1)*opts.itemsPerPage) || 0;
const limit = Math.abs(opts.limit) || Math.abs(Number(opts.itemsPerPage)) || null;
let res;
if (type == "source") {
const geometry = (opts.wgs84 || opts.lonlat || opts.latlon)
? "ST_Transform(geometry, 4326)::json geometry"
: "geometry::json";
const restriction = line
? "(sailline = $3 OR line = $3)"
: "(TRUE OR $3)";
const text = `
SELECT
sailline, sailline_ntba, line, point, ntba,
${geometry},
meta
FROM preplot_saillines_points
WHERE ${restriction}
ORDER BY ${sortKey} ${sortDir}
OFFSET $1
LIMIT $2;
`;
const values = [offset, limit, line];
res = await client.query(text, values);
} else if (type == "sail") {
const geometry = (opts.wgs84 || opts.lonlat || opts.latlon)
? "ST_Transform(pp.geometry, 4326)::json geometry"
: "pp.geometry::json";
const restriction = line
? "(sailline = $3 OR psl.line = $3)"
: "(TRUE OR $3)";
const text = `
WITH psl AS (
SELECT DISTINCT sailline, ntba
FROM preplot_saillines
)
SELECT
psl.sailline, pp.point, pp.ntba, psl.ntba sailline_ntba, pp.meta, ${geometry}
FROM preplot_points pp
INNER JOIN psl ON psl.sailline = pp.line
WHERE pp.class = 'V'
AND ${restriction}
ORDER BY ${sortKey} ${sortDir}
OFFSET $1
LIMIT $2;
`;
const values = [offset, limit, line];
res = await client.query(text, values);
}
client.release();
if (opts.project) {
const tokens = opts.project.split(/\s*[,;:\s]\s*/).filter(e => e.length);
const project = tokens.map(i => i.replace(/^([^.]+)\..*$/, "$1"));
return res.rows.map( r =>
Object.fromEntries(Object.entries(r).filter(entry => project.includes(entry[0])))
);
} else {
return res.rows;
}
}
module.exports = get;