feat: ask for vessels by area

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.
This commit is contained in:
Sameh Khamis committed 2026-10-03 22:44:45 -07:00
1 parent 88b8406073
commit e8e49a49e0
11 files changed
+240 -8

No files matched your search

+24 -6
View File
@@ -6,6 +6,7 @@ import {
AREA_SCHEMA,
areaCenter,
areaContains,
areaRadiusKm,
distanceKm,
resolveArea,
} from '../area.js';
@@ -48,12 +49,29 @@ function vesselRow(record, from) {
}
/**
* Every vessel the source retains. The source's default is the newest few
* thousand, which can leave out a vessel the server still holds; asking for
* more than any server keeps lets the server return all of them.
* Every vessel the source retains, or with `area`, every vessel around it.
* The source's default is the newest few thousand, which can leave out a
* vessel the server still holds; asking for more than any server keeps lets
* the server return all of them. The area lets a server answer for that
* place alone; callers still keep only the vessels inside it.
*/
function readVessels(services, signal) {
return services.vessels.getSnapshot({ maxRows: ALL_VESSELS }, { signal });
function readVessels(services, signal, area) {
const center = area ? areaCenter(area) : null;
return services.vessels.getSnapshot(
{
maxRows: ALL_VESSELS,
...(center
? {
area: {
lat: center.lat,
lon: center.lon,
radiusKm: areaRadiusKm(area),
},
}
: {}),
},
{ signal },
);
}
function snapshotInfo(snapshot) {
@@ -90,7 +108,7 @@ export const vesselsInArea = defineTool({
async run(args, { services, signal }) {
const area = await resolveArea(args.area, { services, signal });
const center = areaCenter(area);
const snapshot = await readVessels(services, signal);
const snapshot = await readVessels(services, signal, area);
const wanted = args.type?.toLowerCase();
const rows = snapshot.records
.filter(
+23
View File
@@ -201,3 +201,26 @@ test('vessel searches ask for every vessel the server retains', async () => {
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);
});