mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:07:08 +00:00
Add new /vessel/track endpoints.
This is a variation on /navdata but returns data more suitable for plotting vessel tracks on the map.
This commit is contained in:
@@ -311,6 +311,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 ],
|
||||
|
||||
@@ -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'),
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
4
lib/www/server/api/middleware/vessel/track/index.js
Normal file
4
lib/www/server/api/middleware/vessel/track/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
module.exports = {
|
||||
project: require('./project'),
|
||||
navdata: require('./navdata')
|
||||
navdata: require('./navdata'),
|
||||
vesseltrack: require('./vesseltrack'),
|
||||
// line: require('./line')
|
||||
};
|
||||
|
||||
191
lib/www/server/lib/db/gis/vesseltrack/get.js
Normal file
191
lib/www/server/lib/db/gis/vesseltrack/get.js
Normal file
@@ -0,0 +1,191 @@
|
||||
const { pool } = require('../../connection');
|
||||
const { project } = require('../../utils');
|
||||
|
||||
async function get(options = {}) {
|
||||
/*
|
||||
* ts0: earliest timestamp (default: NOW - 7 days)
|
||||
* ts1: latest timestamp (if null, assume NOW)
|
||||
* di: decimation interval (return every di-th record, if null: no decimation)
|
||||
* l: limit (return no more than l records, default: 1000, max: 1,000,000)
|
||||
* geojson: 'LineString' (GeoJSON LineString Feature), 'Point' (GeoJSON FeatureCollection),
|
||||
* truthy (array of Point features), falsy (array of {x, y, tstamp, meta})
|
||||
*/
|
||||
let { l, di, ts1, ts0, geojson, projection } = {
|
||||
l: 1000,
|
||||
di: null,
|
||||
ts1: null,
|
||||
ts0: null,
|
||||
geojson: false,
|
||||
...options
|
||||
};
|
||||
|
||||
// Input validation and sanitization
|
||||
l = Math.max(1, Math.min(parseInt(l) || 1000, 1000000)); // Enforce 1 <= l <= 1,000,000
|
||||
di = di != null ? Math.max(1, parseInt(di)) : null; // Ensure di is positive integer or null
|
||||
ts0 = ts0 ? new Date(ts0).toISOString() : new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); // Default: 7 days ago
|
||||
ts1 = ts1 ? new Date(ts1).toISOString() : null; // Convert to ISO string or null
|
||||
geojson = geojson === 'LineString' || geojson === 'Point' ? geojson : !!geojson; // Normalize geojson
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
// Build the WHERE clause and values array dynamically
|
||||
let whereClauses = [];
|
||||
let values = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (ts0) {
|
||||
whereClauses.push(`tstamp >= $${paramIndex++}`);
|
||||
values.push(ts0);
|
||||
}
|
||||
if (ts1) {
|
||||
whereClauses.push(`tstamp <= $${paramIndex++}`);
|
||||
values.push(ts1);
|
||||
}
|
||||
|
||||
// Add limit to values
|
||||
values.push(l);
|
||||
const limitClause = `LIMIT $${paramIndex++}`;
|
||||
|
||||
// Base query with conditional geometry selection
|
||||
let queryText = `
|
||||
SELECT
|
||||
tstamp,
|
||||
CASE
|
||||
WHEN meta->>'lineStatus' = 'offline' THEN
|
||||
ARRAY[COALESCE((meta->>'longitude')::float, ST_X(geometry)), COALESCE((meta->>'latitude')::float, ST_Y(geometry))]
|
||||
ELSE
|
||||
ARRAY[
|
||||
COALESCE(
|
||||
(meta->>'longitudeMaster')::float,
|
||||
ST_X(geometry)
|
||||
),
|
||||
COALESCE(
|
||||
(meta->>'latitudeMaster')::float,
|
||||
ST_Y(geometry)
|
||||
)
|
||||
]
|
||||
END AS coordinates,
|
||||
meta::json AS meta
|
||||
FROM public.real_time_inputs
|
||||
${whereClauses.length ? 'WHERE ' + whereClauses.join(' AND ') : ''}
|
||||
ORDER BY tstamp DESC
|
||||
${limitClause}
|
||||
`;
|
||||
|
||||
// If decimation is requested, wrap the query in a subquery with ROW_NUMBER
|
||||
if (di != null && di > 1) {
|
||||
values.push(di);
|
||||
queryText = `
|
||||
SELECT tstamp, coordinates, meta
|
||||
FROM (
|
||||
SELECT
|
||||
tstamp,
|
||||
CASE
|
||||
WHEN meta->>'lineStatus' = 'offline' THEN
|
||||
ARRAY[COALESCE((meta->>'longitude')::float, ST_X(geometry)), COALESCE((meta->>'latitude')::float, ST_Y(geometry))]
|
||||
ELSE
|
||||
ARRAY[
|
||||
COALESCE(
|
||||
(meta->>'longitudeMaster')::float,
|
||||
ST_X(geometry)
|
||||
),
|
||||
COALESCE(
|
||||
(meta->>'latitudeMaster')::float,
|
||||
ST_Y(geometry)
|
||||
)
|
||||
]
|
||||
END AS coordinates,
|
||||
meta::json AS meta,
|
||||
ROW_NUMBER() OVER (ORDER BY tstamp DESC) AS rn
|
||||
FROM public.real_time_inputs
|
||||
${whereClauses.length ? 'WHERE ' + whereClauses.join(' AND ') : ''}
|
||||
) sub
|
||||
WHERE rn % $${paramIndex} = 0
|
||||
ORDER BY tstamp DESC
|
||||
${limitClause}
|
||||
`;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await client.query(queryText, values);
|
||||
if (!res.rows?.length) {
|
||||
throw { status: 204 }; // No Content
|
||||
}
|
||||
|
||||
// Process rows: Convert tstamp to Date and extract coordinates
|
||||
let processed = res.rows.map(row => ({
|
||||
tstamp: new Date(row.tstamp),
|
||||
x: row.coordinates[0], // Longitude
|
||||
y: row.coordinates[1], // Latitude
|
||||
meta: projection != null
|
||||
? projection == ""
|
||||
? undefined
|
||||
: project([row.meta], projection)[0] : row.meta
|
||||
}));
|
||||
|
||||
// Handle geojson output formats
|
||||
if (geojson === 'LineString') {
|
||||
// Compute line length (haversine formula in JavaScript for simplicity)
|
||||
let length = 0;
|
||||
for (let i = 1; i < processed.length; i++) {
|
||||
const p1 = processed[i - 1];
|
||||
const p2 = processed[i];
|
||||
const R = 6371e3; // Earth's radius in meters
|
||||
const φ1 = p1.y * Math.PI / 180;
|
||||
const φ2 = p2.y * Math.PI / 180;
|
||||
const Δφ = (p2.y - p1.y) * Math.PI / 180;
|
||||
const Δλ = (p2.x - p1.x) * Math.PI / 180;
|
||||
const a = Math.sin(Δφ / 2) ** 2 +
|
||||
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
length += R * c;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: processed.map(p => [p.x, p.y])
|
||||
},
|
||||
properties: {
|
||||
ts0: processed[processed.length - 1].tstamp.toISOString(),
|
||||
ts1: processed[0].tstamp.toISOString(),
|
||||
distance: length
|
||||
}
|
||||
};
|
||||
} else if (geojson === 'Point') {
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: processed.map(p => ({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [p.x, p.y]
|
||||
},
|
||||
properties: {
|
||||
...p.meta,
|
||||
tstamp: p.tstamp.toISOString()
|
||||
}
|
||||
}))
|
||||
};
|
||||
} else if (geojson) {
|
||||
return processed.map(p => ({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [p.x, p.y]
|
||||
},
|
||||
properties: {
|
||||
...p.meta,
|
||||
tstamp: p.tstamp.toISOString()
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
return processed;
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = get;
|
||||
5
lib/www/server/lib/db/gis/vesseltrack/index.js
Normal file
5
lib/www/server/lib/db/gis/vesseltrack/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get')
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user