diff --git a/lib/www/server/api/index.js b/lib/www/server/api/index.js index 7281a95..dcba6f9 100644 --- a/lib/www/server/api/index.js +++ b/lib/www/server/api/index.js @@ -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 ], diff --git a/lib/www/server/api/middleware/index.js b/lib/www/server/api/middleware/index.js index 645179b..2c6ad51 100644 --- a/lib/www/server/api/middleware/index.js +++ b/lib/www/server/api/middleware/index.js @@ -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'), diff --git a/lib/www/server/api/middleware/vessel/index.js b/lib/www/server/api/middleware/vessel/index.js new file mode 100644 index 0000000..85bbcc1 --- /dev/null +++ b/lib/www/server/api/middleware/vessel/index.js @@ -0,0 +1,4 @@ + +module.exports = { + track: require('./track'), +} diff --git a/lib/www/server/api/middleware/vessel/track/get.js b/lib/www/server/api/middleware/vessel/track/get.js new file mode 100644 index 0000000..2084ee3 --- /dev/null +++ b/lib/www/server/api/middleware/vessel/track/get.js @@ -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); + } +} diff --git a/lib/www/server/api/middleware/vessel/track/index.js b/lib/www/server/api/middleware/vessel/track/index.js new file mode 100644 index 0000000..cf36c60 --- /dev/null +++ b/lib/www/server/api/middleware/vessel/track/index.js @@ -0,0 +1,4 @@ + +module.exports = { + get: require('./get'), +} diff --git a/lib/www/server/lib/db/gis/index.js b/lib/www/server/lib/db/gis/index.js index 184d958..22e5bee 100644 --- a/lib/www/server/lib/db/gis/index.js +++ b/lib/www/server/lib/db/gis/index.js @@ -1,6 +1,7 @@ module.exports = { project: require('./project'), - navdata: require('./navdata') + navdata: require('./navdata'), + vesseltrack: require('./vesseltrack'), // line: require('./line') }; diff --git a/lib/www/server/lib/db/gis/vesseltrack/get.js b/lib/www/server/lib/db/gis/vesseltrack/get.js new file mode 100644 index 0000000..56cc8ec --- /dev/null +++ b/lib/www/server/lib/db/gis/vesseltrack/get.js @@ -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; diff --git a/lib/www/server/lib/db/gis/vesseltrack/index.js b/lib/www/server/lib/db/gis/vesseltrack/index.js new file mode 100644 index 0000000..2a02b1c --- /dev/null +++ b/lib/www/server/lib/db/gis/vesseltrack/index.js @@ -0,0 +1,5 @@ + +module.exports = { + get: require('./get') +}; +