feat: return the view that shows each answer

Tool answers with something to show now include data.view: the view with
the matching layers on, the area framed from above or a single aircraft or
satellite followed, and the link that opens it. Area framing moves to
src/tools/views.js so links and answers frame alike.
This commit is contained in:
Sameh Khamis committed 2026-10-02 15:21:36 -07:00
1 parent 96a8a9addb
commit d6473b6049
20 files changed
+393 -34

No files matched your search

+26
View File
@@ -1,5 +1,6 @@
/** Weather map imagery (radar, satellite, lightning) and wind at a point. */
import { suggestView } from '../views.js';
import { windFrom } from '../../layers/wind/inspection.js';
import { sampleWind, windSpeed } from '../../layers/wind/model.js';
import { defineTool, ToolError } from '../catalog.js';
@@ -20,6 +21,13 @@ const MAPS = {
global_satellite: 'clouds',
lightning: 'lightning',
};
/** The app layer that shows each map. */
const MAP_LAYERS = {
radar: 'weather-radar',
satellite: 'weather-satellite',
global_satellite: 'weather-satellite',
lightning: 'weather-lightning',
};
const IMAGE_SIZE = { width: 1024, height: 512 };
const SNAP_DEGREES = 0.25;
const MIN_HALF_HEIGHT_DEGREES = 0.5;
@@ -136,6 +144,19 @@ export const getWeatherMap = defineTool({
return {
summary: `${snapshot.title || args.map} at ${snapshot.latest}${where}.`,
data: {
view: suggestView(
services,
area
? { area, layers: [MAP_LAYERS[args.map]] }
: {
point: {
lat: (snapshot.bounds.south + snapshot.bounds.north) / 2,
lon: (snapshot.bounds.west + snapshot.bounds.east) / 2,
},
altitudeM: 8_000_000,
layers: [MAP_LAYERS[args.map]],
},
),
map: args.map,
title: snapshot.title ?? null,
description: snapshot.description ?? null,
@@ -188,6 +209,11 @@ export const getWind = defineTool({
`Wind at ${point.label}: ${kph.toFixed(0)} km/h` +
(from === 'Calm' ? ', calm.' : ` from the ${from}.`),
data: {
view: suggestView(services, {
point,
altitudeM: 2_000_000,
layers: ['wind'],
}),
location: point,
speed_mps: round(speed, 1),
speed_kph: round(kph, 1),
+42 -7
View File
@@ -1,5 +1,6 @@
/** Aviation queries over the live aircraft sources (src/sources/live). */
import { suggestView } from '../views.js';
import { defineTool, ToolError } from '../catalog.js';
import {
AREA_SCHEMA,
@@ -129,7 +130,14 @@ export const aircraftInArea = defineTool({
summary:
`${rows.length} ${kind} in ${area.label}` +
(notes.length ? ` (${notes.join('; ')}).` : '.'),
data: { ...capRows(rows, args.limit), ...snapshotInfo(snapshot) },
data: {
view: suggestView(services, {
area,
layers: [args.military ? 'military' : 'flights'],
}),
...capRows(rows, args.limit),
...snapshotInfo(snapshot),
},
};
},
});
@@ -190,17 +198,25 @@ export const findAircraft = defineTool({
.map(({ name }) => name);
if (unavailable.length === feeds.length) throw settled[0].reason;
const seen = new Set();
const rows = settled
.filter((result) => result.status === 'fulfilled')
.flatMap((result) => result.value.records)
const matches = settled
.flatMap((result, index) =>
result.status === 'fulfilled'
? result.value.records.map((record) => ({
record,
feed: feeds[index].name,
}))
: [],
)
.filter(
(record) =>
({ record }) =>
String(record[field] || '')
.trim()
.toUpperCase() === wanted,
)
.filter((record) => !seen.has(record.id) && seen.add(record.id))
.map((record) => aircraftRow(record));
.filter(({ record }) => !seen.has(record.id) && seen.add(record.id));
const rows = matches.map(({ record }) => aircraftRow(record));
// One match is shown followed; several are not framed.
const only = matches.length === 1 ? matches[0] : null;
// Each feed that answered, with its own freshness and coverage.
const answered = feeds.flatMap(({ name }, index) =>
settled[index].status === 'fulfilled'
@@ -222,6 +238,16 @@ export const findAircraft = defineTool({
? ` The ${stale.join(' and ')} feed data may be stale.`
: ''),
data: {
view: only
? suggestView(services, {
point: { lat: only.record.latitude, lon: only.record.longitude },
follow: {
kind:
only.feed === 'military' ? 'military_aircraft' : 'aircraft',
id: only.record.id,
},
})
: null,
...capRows(rows, args.limit),
unavailable_feeds: unavailable,
stale_feeds: stale,
@@ -264,6 +290,15 @@ export const getAircraftTrack = defineTool({
? `${countNoun(points.length, 'position')} for ${icao24} from ${isoTime(first.observedAtMs)} to ${isoTime(last.observedAtMs)}.`
: `No recent track is available for ${icao24}.`,
data: {
view: last
? suggestView(services, {
point: { lat: last.latitude, lon: last.longitude },
follow: {
kind: args.military ? 'military_aircraft' : 'aircraft',
id: icao24,
},
})
: null,
icao24,
total: points.length,
returned: kept.length,
+20 -1
View File
@@ -1,5 +1,6 @@
/** Composite queries that combine other tools' answers for one area. */
import { suggestView } from '../views.js';
import { feedProvenanceEnvelope } from '../../data/layerSnapshot.js';
import { defineTool, ToolError } from '../catalog.js';
import {
@@ -28,30 +29,35 @@ const SECTIONS = [
},
{
key: 'earthquakes',
layer: 'earthquakes',
label: 'Earthquakes',
tool: 'get_earthquakes',
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'fires',
layer: 'local-firms',
label: 'Active fires',
tool: 'get_active_fires',
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'aircraft',
layer: 'flights',
label: 'Aircraft',
tool: 'aircraft_in_area',
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'vessels',
layer: 'ais-live-vessels',
label: 'Ships',
tool: 'vessels_in_area',
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'cyclones',
layer: 'weather-cyclones',
label: 'Tropical cyclones',
tool: 'get_cyclones',
args: (area) => ({ area: area.argument }),
@@ -187,7 +193,16 @@ export const situationBrief = defineTool({
const { area, brief, lines } = await buildBrief(args, context);
return {
summary: `Situation in ${area.label}: ${lines.join(' ')}`,
data: { area: area.label, sections: brief },
data: {
view: suggestView(context.services, {
area,
layers: SECTIONS.filter(({ key }) => brief[key])
.map(({ layer }) => layer)
.filter(Boolean),
}),
area: area.label,
sections: brief,
},
};
},
});
@@ -277,6 +292,10 @@ export const militaryAwareness = defineTool({
return {
summary: `Military awareness within ${radiusKm} km of ${point.label}: ${lines.join(' ')}`,
data: {
view: suggestView(services, {
area,
layers: ['military', 'ais-live-vessels', 'military-installations'],
}),
location: point,
radius_km: radiusKm,
sections: answers,
+11
View File
@@ -1,5 +1,6 @@
/** Submarine cables and their landing points. */
import { suggestView } from '../views.js';
import { defineTool, ToolError } from '../catalog.js';
import {
AREA_SCHEMA,
@@ -80,6 +81,16 @@ export const findSubmarineCables = defineTool({
? `, with ${countNoun(landingRows.length, 'landing point')}.`
: '.'),
data: {
view: suggestView(
services,
area
? { area, layers: ['telegeography-submarine-cables'] }
: {
point: { lat: 20, lon: 0 },
altitudeM: 15_000_000,
layers: ['telegeography-submarine-cables'],
},
),
...capRows(rows, args.limit),
landing_points: area ? capRows(landingRows, args.limit) : null,
attribution: ATTRIBUTION,
+19 -1
View File
@@ -3,6 +3,7 @@
* terrain height, mapped military sites and map features.
*/
import { suggestView } from '../views.js';
import { defineTool, ToolError } from '../catalog.js';
import {
AREA_SCHEMA,
@@ -221,6 +222,16 @@ export const getCyclones = defineTool({
? `; strongest ${storms[0].name} at ${storms[0].wind_kt} kt.`
: '.'),
data: {
view: suggestView(
services,
area
? { area, layers: ['weather-cyclones'] }
: {
point: { lat: 20, lon: 0 },
altitudeM: 15_000_000,
layers: ['weather-cyclones'],
},
),
storms,
coverage: snapshot.coverage ?? null,
source: snapshot.source ?? null,
@@ -269,7 +280,10 @@ export const getFirePerimeters = defineTool({
.sort((a, b) => (b.acres ?? 0) - (a.acres ?? 0));
return {
summary: `${countNoun(rows.length, 'mapped wildfire perimeter')} in ${area.label}.`,
data: capRows(rows, args.limit),
data: {
view: suggestView(services, { area, layers: ['fire-perimeters'] }),
...capRows(rows, args.limit),
},
};
},
});
@@ -436,6 +450,10 @@ export const findMilitaryInstallations = defineTool({
`${countNoun(rows.length, 'mapped military installation')} in ${area.label}` +
(notes.length ? ` (${notes.join('; ')}).` : '.'),
data: {
view: suggestView(services, {
area,
layers: ['military-installations'],
}),
...capRows(rows, args.limit),
complete,
names_pending: namesPending,
+17
View File
@@ -1,5 +1,6 @@
/** The bundled Bhote Koshi 2026 outburst flood event pack. */
import { suggestView } from '../views.js';
import { BHOTE_KOSHI_FLOOD_PATH } from '../../data/bhoteKoshiFloodPath.js';
import { defineTool, ToolError } from '../catalog.js';
import { distanceKm } from '../area.js';
@@ -26,6 +27,18 @@ function pathLengthKm(path) {
const pathPoint = ([lon, lat]) => ({ lat, lon });
/** The box around a `[lon, lat]` path, as an area to frame. */
function pathArea(path) {
const lons = path.map(([lon]) => lon);
const lats = path.map(([, lat]) => lat);
return {
west: Math.min(...lons),
south: Math.min(...lats),
east: Math.max(...lons),
north: Math.max(...lats),
};
}
function evidenceRow(record) {
return {
sequence: record.sequence ?? null,
@@ -88,6 +101,10 @@ export const getBhoteKoshiFlood = defineTool({
(args.phase ? ` in the ${args.phase} phase` : '') +
` along a ${lengthKm} km mapped flood path.`,
data: {
view: suggestView(services, {
area: pathArea(BHOTE_KOSHI_FLOOD_PATH),
layers: ['bhote-koshi-2026'],
}),
id: event.id,
title: event.title,
observed_date: event.observedDate,
+12
View File
@@ -1,5 +1,6 @@
/** Natural hazard queries: earthquakes and active fires. */
import { suggestView } from '../views.js';
import { defineTool, ToolError } from '../catalog.js';
import { AREA_SCHEMA, areaContains, resolveArea } from '../area.js';
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
@@ -38,6 +39,16 @@ export const getEarthquakes = defineTool({
return {
summary: `${countNoun(events.length, 'earthquake')} of M${minimum}+ in the last 24 hours${where}${strongest}.`,
data: {
view: suggestView(
services,
area
? { area, layers: ['earthquakes'] }
: {
point: { lat: 20, lon: 0 },
altitudeM: 15_000_000,
layers: ['earthquakes'],
},
),
...result,
rows: result.rows.map((event) => ({
id: event.usgsId ?? event.stableId,
@@ -93,6 +104,7 @@ export const getActiveFires = defineTool({
`${countNoun(fires.length, 'fire detection')} in ${area.label} in the last 24 hours` +
(notes.length ? ` (${notes.join('; ')}).` : '.'),
data: {
view: suggestView(services, { area, layers: ['local-firms'] }),
stale,
missing_sources: missing,
fetched_at: isoTime(snapshot.fetchedAt),
+2
View File
@@ -1,5 +1,6 @@
/** The most recent satellite image of an area. */
import { suggestView } from '../views.js';
import { defineTool, ToolError } from '../catalog.js';
import { AREA_SCHEMA, areaRadiusKm, resolveArea } from '../area.js';
import { toBase64 } from '../results.js';
@@ -105,6 +106,7 @@ export const getRecentImagery = defineTool({
return {
summary: `Most recent image of ${area.label}: ${sensor}, ${candidate.day}, ${quality}${reason === 'overview' ? '' : `, ${cloud}`}.`,
data: {
view: suggestView(services, { area, layers: ['recent-imagery'] }),
area: area.label,
image: {
product: candidate.product,
+2
View File
@@ -1,5 +1,6 @@
/** Bundled OpenStreetMap infrastructure: datacenters and dams. */
import { suggestView } from '../views.js';
import { defineTool } from '../catalog.js';
import {
AREA_SCHEMA,
@@ -62,6 +63,7 @@ export const findInfrastructure = defineTool({
return {
summary: `${countNoun(rows.length, `mapped ${noun}`)} in ${area.label}.`,
data: {
view: suggestView(services, { area, layers: [layerId] }),
...capRows(rows, args.limit),
attribution: ATTRIBUTION[args.kind],
},
+26 -2
View File
@@ -1,5 +1,6 @@
/** Maritime queries over the live vessel source (src/sources/live). */
import { suggestView } from '../views.js';
import { defineTool, ToolError } from '../catalog.js';
import {
AREA_SCHEMA,
@@ -104,7 +105,11 @@ export const vesselsInArea = defineTool({
summary:
`${countNoun(rows.length, kind)} in ${area.label}` +
(snapshot.freshness === 'stale' ? ' (data may be stale).' : '.'),
data: { ...capRows(rows, args.limit), ...snapshotInfo(snapshot) },
data: {
view: suggestView(services, { area, layers: ['ais-live-vessels'] }),
...capRows(rows, args.limit),
...snapshotInfo(snapshot),
},
};
},
});
@@ -148,7 +153,19 @@ export const findVessel = defineTool({
summary: rows.length
? `Found ${countNoun(rows.length, 'vessel')} with ${key} ${args[key]}.`
: `No vessel with ${key} ${args[key]} is currently reported.`,
data: { ...capRows(rows, args.limit), ...snapshotInfo(snapshot) },
data: {
// Ships cannot be followed from a link; one match is framed closely.
view:
rows.length === 1
? suggestView(services, {
point: { lat: rows[0].lat, lon: rows[0].lon },
layers: ['ais-live-vessels'],
altitudeM: 5_000,
})
: null,
...capRows(rows, args.limit),
...snapshotInfo(snapshot),
},
};
},
});
@@ -177,6 +194,13 @@ export const getVesselTrack = defineTool({
? `${countNoun(points.length, 'position')} for ${args.mmsi} from ${isoTime(points[0].observedAtMs)} to ${isoTime(points.at(-1).observedAtMs)}.`
: `No recent track is available for ${args.mmsi}.`,
data: {
view: kept.length
? suggestView(services, {
point: { lat: kept.at(-1).latitude, lon: kept.at(-1).longitude },
layers: ['ais-live-vessels'],
altitudeM: 20_000,
})
: null,
mmsi: args.mmsi,
total: points.length,
returned: kept.length,
+8
View File
@@ -1,5 +1,6 @@
/** Public camera and radio queries over the CCTV and radio sources. */
import { suggestView } from '../views.js';
import { defineTool, ToolError } from '../catalog.js';
import {
AREA_SCHEMA,
@@ -106,6 +107,7 @@ export const findCctvCameras = defineTool({
.join(', ')}).`
: '.'),
data: {
view: suggestView(services, { area, layers: ['cctv'] }),
...capRows(rows, args.limit),
complete: trimmed.length === 0,
catalog_trimmed: trimmed,
@@ -150,6 +152,11 @@ export const getCctvSnapshot = defineTool({
return {
summary: `Current view from ${name}${camera.city ? ` in ${camera.city}` : ''}${camera.credit ? `, courtesy of ${camera.credit}` : ''}.`,
data: {
view: suggestView(services, {
point: { lat: camera.lat, lon: camera.lon },
altitudeM: 1_500,
layers: ['cctv'],
}),
id: camera.id,
name,
city: text(camera.city),
@@ -247,6 +254,7 @@ export const findRadioStations = defineTool({
return {
summary: `${countNoun(rows.length, 'radio station')}${what}${where} (${notes.join('; ')}).`,
data: {
view: area ? suggestView(services, { area, layers: ['radio'] }) : null,
...capRows(rows, args.limit),
directory_size: directory.stations.length,
stale: directory.stale === true,
+4
View File
@@ -1,5 +1,6 @@
/** Bike-share and public transit queries. */
import { suggestView } from '../views.js';
import {
parseStationInformation,
parseStationStatus,
@@ -134,6 +135,7 @@ export const getBikeShare = defineTool({
`${countNoun(bikes, 'bike')} available` +
(notes.length ? ` (${notes.join('; ')}).` : '.'),
data: {
view: suggestView(services, { area, layers: ['bikeshare'] }),
...capRows(rows, args.limit),
systems: systems.map((system) => system.city),
systems_not_searched: skipped.map((system) => system.city),
@@ -275,6 +277,7 @@ export const getTransitVehicles = defineTool({
`${countNoun(rows.length, 'transit vehicle')}${what} in ${area.label}` +
(notes.length ? ` (${notes.join('; ')}).` : '.'),
data: {
view: suggestView(services, { area, layers: ['transit'] }),
...capRows(rows, args.limit),
stale: staleFeeds > 0,
expired_positions_dropped: expired,
@@ -472,6 +475,7 @@ export const getTrafficFlow = defineTool({
? ' Some map tiles did not load, so these figures are partial.'
: ''),
data: {
view: suggestView(services, { area, layers: ['traffic'] }),
area: area.label,
partial: flow.partial,
speed_pct_of_free_flow: speedPct,
+3 -19
View File
@@ -2,12 +2,8 @@
import { VIEW_PROPERTIES, createView, viewUrl } from '../../view/index.js';
import { defineTool, ToolError } from '../catalog.js';
import { AREA_SCHEMA, areaCenter, areaRadiusKm, resolveArea } from '../area.js';
const MIN_ALTITUDE_M = 500;
const MAX_ALTITUDE_M = 15_000_000;
// A top-down view sees roughly this many meters of ground per meter of altitude.
const GROUND_PER_ALTITUDE = 0.55;
import { AREA_SCHEMA, resolveArea } from '../area.js';
import { cameraForArea } from '../views.js';
/** Tool arguments that describe a view: an area to frame, or a camera. */
export const VIEW_ARGUMENTS = Object.freeze({
@@ -24,20 +20,8 @@ export async function resolveViewArguments(args, { services, signal }) {
let label = null;
if (args.area) {
const area = await resolveArea(args.area, { services, signal });
const center = areaCenter(area);
label = area.label;
camera = {
lat: center.lat,
lon: center.lon,
altitude_m: Math.min(
MAX_ALTITUDE_M,
Math.max(
MIN_ALTITUDE_M,
(areaRadiusKm(area) * 1000) / GROUND_PER_ALTITUDE,
),
),
...camera,
};
camera = { ...cameraForArea(area), ...camera };
}
if (!Number.isFinite(camera.lat) || !Number.isFinite(camera.lon))
throw new ToolError(
+28 -1
View File
@@ -1,5 +1,6 @@
/** Space queries: launches, satellite passes and satellites overhead. */
import { suggestView } from '../views.js';
import { twoline2satrec } from 'satellite.js';
import {
findNextSatellitePass,
@@ -57,7 +58,15 @@ export const getRecentLaunches = defineTool({
summary:
`${countNoun(rows.length, 'launch', 'launches')} in the last 30 days${rows[0] ? `; latest ${rows[0].name}` : ''}` +
(stale ? ' (data may be stale).' : '.'),
data: { ...result, stale },
data: {
view: suggestView(services, {
point: { lat: 20, lon: 0 },
altitudeM: 15_000_000,
layers: ['rocket-launches'],
}),
...result,
stale,
},
};
},
});
@@ -103,6 +112,17 @@ const round = (value, digits) =>
Number.isFinite(value) ? Number(value.toFixed(digits)) : null;
const nowMs = (services) => services.clock?.now() ?? Date.now();
/** A view following a satellite from above an observer. */
function followSatellite(services, point, satellite) {
return satellite.norad
? suggestView(services, {
point,
altitudeM: 3_000_000,
follow: { kind: 'satellite', id: String(satellite.norad) },
})
: null;
}
async function readCatalog(services, group, signal) {
const result = await services.satellites.readGroup(group, { signal });
if (!result.ok)
@@ -197,6 +217,7 @@ export const nextSatellitePass = defineTool({
return {
summary: `${name} has no ${args.visible_only ? 'visible ' : ''}pass over ${point.label} in the next ${hours} hours${staleNote}.`,
data: {
view: followSatellite(services, point, satellite),
stale,
location: point,
satellite: name,
@@ -210,6 +231,7 @@ export const nextSatellitePass = defineTool({
`peaking at ${Math.round(pass.maxElevDeg)}°` +
`${pass.visible ? '; visible to the naked eye' : ''}${staleNote}.`,
data: {
view: followSatellite(services, point, satellite),
stale,
location: point,
satellite: name,
@@ -273,6 +295,11 @@ export const satellitesOverhead = defineTool({
`${countNoun(rows.length, 'satellite')} from the ${group} group ${rows.length === 1 ? 'is' : 'are'} at least ${minimum}° above ${point.label} at ${isoTime(at)}` +
(stale ? ' (orbit data may be stale).' : '.'),
data: {
view: suggestView(services, {
point,
altitudeM: 3_000_000,
layers: ['satellites'],
}),
...capRows(rows, args.limit),
stale,
location: point,
+2
View File
@@ -1,5 +1,6 @@
/** Automated license plate reader cameras mapped in OpenStreetMap. */
import { suggestView } from '../views.js';
import { defineTool, ToolError } from '../catalog.js';
import {
AREA_SCHEMA,
@@ -88,6 +89,7 @@ export const findAlprCameras = defineTool({
`${countNoun(rows.length, 'license plate reader camera')} mapped in ${area.label}` +
(notes.length ? ` (${notes.join('; ')}).` : '.'),
data: {
view: suggestView(services, { area, layers: ['alpr-cameras'] }),
...capRows(rows, args.limit),
// Some tiles failed, or a dense area was trimmed to the source limit.
complete: !result.saturated,