mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
The vessel source accepts an optional area and sends it as lat, lon and radius_km. The vessel layer asks for the area the camera shows (centered on the middle of the screen, sized by camera height), or every vessel from high enough up, and asks again when the view moves or zooms by a quarter of that radius. vessels_in_area names its area too. Servers that hold every vessel may ignore the area; callers still keep only what is inside it.
227 lines
6.5 KiB
JavaScript
227 lines
6.5 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { composeCatalog, coreTools } from '../index.js';
|
|
import { LiveSourceError } from '../../sources/live/contract.js';
|
|
import { createAisStreamSource } from '../../sources/live/standalone.js';
|
|
import { AISSTREAM_CACHE_MAX } from '../../../server/providers/vessels/ais-store.js';
|
|
|
|
const vessel = (id, latitude, longitude, extra = {}) => ({
|
|
id,
|
|
reference: id,
|
|
latitude,
|
|
longitude,
|
|
name: `SHIP ${id}`,
|
|
imo: '',
|
|
type: 'Cargo',
|
|
destination: '',
|
|
speedMps: 6.17,
|
|
courseDeg: 181.2,
|
|
headingDeg: 180,
|
|
observedAtMs: Date.UTC(2026, 0, 1),
|
|
altitudeDatum: 'sea-surface',
|
|
...extra,
|
|
});
|
|
const records = [
|
|
vessel('366999712', 37.81, -122.41, {
|
|
name: 'GOLDEN BEAR',
|
|
imo: '9123456',
|
|
type: 'Passenger',
|
|
}),
|
|
vessel('538001234', 37.75, -122.3, {
|
|
name: 'PACIFIC TRADER',
|
|
imo: 'IMO7654321',
|
|
type: 'Tanker',
|
|
destination: 'OAKLAND',
|
|
}),
|
|
vessel('235000001', 51.5, 1.5),
|
|
];
|
|
const vessels = (extra = {}) => ({
|
|
async getSnapshot() {
|
|
return {
|
|
records,
|
|
source: 'AISStream',
|
|
coverage: 'received AIS positions',
|
|
observedAtMs: Date.UTC(2026, 0, 1),
|
|
freshness: 'current',
|
|
...extra,
|
|
};
|
|
},
|
|
async getTrack(mmsi) {
|
|
return {
|
|
records: [
|
|
{
|
|
latitude: 37.8,
|
|
longitude: -122.4,
|
|
observedAtMs: Date.UTC(2026, 0, 1, 1),
|
|
},
|
|
{
|
|
latitude: 37.7,
|
|
longitude: -122.5,
|
|
observedAtMs: Date.UTC(2026, 0, 1, 0),
|
|
},
|
|
{ latitude: 37.6, longitude: -122.6, observedAtMs: null },
|
|
].filter(() => mmsi === '366999712'),
|
|
complete: false,
|
|
};
|
|
},
|
|
});
|
|
const bay = { lat: 37.78, lon: -122.4, radius_km: 20 };
|
|
|
|
test('vessels in an area are nearest first and filterable by type', async () => {
|
|
const catalog = composeCatalog({
|
|
tools: coreTools,
|
|
services: { vessels: vessels() },
|
|
});
|
|
const all = await catalog.call('vessels_in_area', { area: bay });
|
|
assert.equal(all.summary, '2 vessels in 20 km around 37.780, -122.400.');
|
|
assert.deepEqual(
|
|
all.data.rows.map((row) => row.mmsi),
|
|
['366999712', '538001234'],
|
|
);
|
|
assert.deepEqual(all.data.rows[0], {
|
|
mmsi: '366999712',
|
|
name: 'GOLDEN BEAR',
|
|
imo: '9123456',
|
|
type: 'Passenger',
|
|
destination: null,
|
|
lat: 37.81,
|
|
lon: -122.41,
|
|
speed_mps: 6.2,
|
|
course_deg: 181,
|
|
heading_deg: 180,
|
|
observed_at: '2026-01-01T00:00:00.000Z',
|
|
distance_km: 3.4,
|
|
});
|
|
assert.equal(all.data.source, 'AISStream');
|
|
const tankers = await catalog.call('vessels_in_area', {
|
|
area: bay,
|
|
type: 'tanker',
|
|
});
|
|
assert.equal(
|
|
tankers.summary,
|
|
'1 tanker vessel in 20 km around 37.780, -122.400.',
|
|
);
|
|
const stale = composeCatalog({
|
|
tools: coreTools,
|
|
services: { vessels: vessels({ freshness: 'stale' }) },
|
|
});
|
|
assert.match(
|
|
(await stale.call('vessels_in_area', { area: bay })).summary,
|
|
/\(data may be stale\)\.$/,
|
|
);
|
|
});
|
|
|
|
test('vessels are found by exactly one identifier', async () => {
|
|
const catalog = composeCatalog({
|
|
tools: coreTools,
|
|
services: { vessels: vessels() },
|
|
});
|
|
assert.deepEqual(
|
|
(await catalog.call('find_vessel', { mmsi: '366999712' })).data.rows.map(
|
|
(row) => row.name,
|
|
),
|
|
['GOLDEN BEAR'],
|
|
);
|
|
assert.deepEqual(
|
|
(await catalog.call('find_vessel', { imo: '7654321' })).data.rows.map(
|
|
(row) => row.mmsi,
|
|
),
|
|
['538001234'],
|
|
);
|
|
const byName = await catalog.call('find_vessel', { name: 'trader' });
|
|
assert.equal(byName.summary, 'Found 1 vessel with name trader.');
|
|
assert.equal(
|
|
(await catalog.call('find_vessel', { mmsi: '99999' })).summary,
|
|
'No vessel with mmsi 99999 is currently reported.',
|
|
);
|
|
await assert.rejects(
|
|
catalog.call('find_vessel', { mmsi: '366999712', name: 'xy' }),
|
|
/exactly one/,
|
|
);
|
|
await assert.rejects(
|
|
catalog.call('find_vessel', { mmsi: 'abc' }),
|
|
/must match/,
|
|
);
|
|
});
|
|
|
|
test('vessel tracks are ordered and drop untimed positions', async () => {
|
|
const catalog = composeCatalog({
|
|
tools: coreTools,
|
|
services: { vessels: vessels() },
|
|
});
|
|
const track = await catalog.call('get_vessel_track', { mmsi: '366999712' });
|
|
assert.equal(
|
|
track.summary,
|
|
'2 positions for 366999712 from 2026-01-01T00:00:00.000Z to 2026-01-01T01:00:00.000Z.',
|
|
);
|
|
assert.deepEqual(
|
|
track.data.points.map((point) => point.lat),
|
|
[37.7, 37.8],
|
|
);
|
|
assert.equal(
|
|
(await catalog.call('get_vessel_track', { mmsi: '235000001' })).summary,
|
|
'No recent track is available for 235000001.',
|
|
);
|
|
});
|
|
|
|
test('a missing AIS key surfaces as an unavailable feed', async () => {
|
|
const keyless = {
|
|
getSnapshot: async () => {
|
|
throw new LiveSourceError('unavailable', 'AISSTREAM_API_KEY not set');
|
|
},
|
|
};
|
|
const catalog = composeCatalog({
|
|
tools: coreTools,
|
|
services: { vessels: keyless },
|
|
});
|
|
await assert.rejects(
|
|
catalog.call('vessels_in_area', { area: bay }),
|
|
(error) =>
|
|
error.code === 'unavailable' &&
|
|
error.message === 'AISSTREAM_API_KEY not set',
|
|
);
|
|
});
|
|
|
|
test('vessel searches ask for every vessel the server retains', async () => {
|
|
const requested = [];
|
|
const source = createAisStreamSource({
|
|
origin: () => 'http://localhost',
|
|
fetchImpl: async (url) => {
|
|
requested.push(new URL(url).searchParams.get('maxRows'));
|
|
return Response.json({ rows: [], source: 'AISStream', status: 'live' });
|
|
},
|
|
});
|
|
const catalog = composeCatalog({
|
|
tools: coreTools,
|
|
services: { vessels: source },
|
|
});
|
|
await catalog.call('find_vessel', { mmsi: '366999712' });
|
|
await catalog.call('vessels_in_area', { area: bay });
|
|
assert.equal(requested.length, 2);
|
|
for (const maxRows of requested)
|
|
assert.ok(Number(maxRows) >= AISSTREAM_CACHE_MAX, maxRows);
|
|
});
|
|
|
|
test('an area search names its area to the server; a vessel lookup asks for every vessel', async () => {
|
|
const requested = [];
|
|
const source = createAisStreamSource({
|
|
origin: () => 'http://localhost',
|
|
fetchImpl: async (url) => {
|
|
requested.push(new URL(url).searchParams);
|
|
return Response.json({ rows: [], source: 'AISStream', status: 'live' });
|
|
},
|
|
});
|
|
const catalog = composeCatalog({
|
|
tools: coreTools,
|
|
services: { vessels: source },
|
|
});
|
|
await catalog.call('vessels_in_area', { area: bay });
|
|
await catalog.call('find_vessel', { mmsi: '366999712' });
|
|
const [area, lookup] = requested;
|
|
assert.equal(area.get('lat'), '37.78000');
|
|
assert.equal(area.get('lon'), '-122.40000');
|
|
assert.equal(area.get('radius_km'), '20.0');
|
|
assert.equal(lookup.has('lat'), false);
|
|
assert.equal(lookup.has('radius_km'), false);
|
|
});
|