mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
fix: tighten tool answers found in local testing
Unknown aircraft tracks read as empty instead of a server error, aircraft info names the aircraft and flight, the HUD caption sends the app's caption context, and satellite pass, satellites overhead and nearby places take a place name or point like the other tools.
This commit is contained in:
1 parent
1a03e7483d
commit
ec1adbdc19
9 files changed
+118
-63
No files matched your search
+3
-3
@@ -140,14 +140,14 @@ public feeds the sources already use.
|
|||||||
| `vessels_in_area` | `vessels` | Ships reported by AIS in an area, nearest first, optionally by type |
|
| `vessels_in_area` | `vessels` | Ships reported by AIS in an area, nearest first, optionally by type |
|
||||||
| `find_vessel` | `vessels` | Ships anywhere by MMSI, IMO number or name |
|
| `find_vessel` | `vessels` | Ships anywhere by MMSI, IMO number or name |
|
||||||
| `get_vessel_track` | `vessels` | Recent positions of one ship, thinned to 200 points |
|
| `get_vessel_track` | `vessels` | Recent positions of one ship, thinned to 200 points |
|
||||||
| `next_satellite_pass` | `satellites` | Next pass over a point (default the ISS), with naked-eye visibility |
|
| `next_satellite_pass` | `satellites` | Next pass over a place or point (default the ISS), with naked-eye visibility |
|
||||||
| `satellites_overhead` | `satellites` | Satellites in a CelesTrak group above a point now, highest first |
|
| `satellites_overhead` | `satellites` | Satellites in a CelesTrak group above a place or point now, highest first |
|
||||||
| `find_cctv_cameras` | `cctv` | Public cameras in an area, nearest first |
|
| `find_cctv_cameras` | `cctv` | Public cameras in an area, nearest first |
|
||||||
| `get_cctv_snapshot` | `cctv` | The current image from one camera, returned as image content |
|
| `get_cctv_snapshot` | `cctv` | The current image from one camera, returned as image content |
|
||||||
| `find_alpr_cameras` | `alpr` | OpenStreetMap-mapped license plate readers in a US/Canadian area up to 3° |
|
| `find_alpr_cameras` | `alpr` | OpenStreetMap-mapped license plate readers in a US/Canadian area up to 3° |
|
||||||
| `find_radio_stations` | `radio` | Radio Browser stations by area and/or search terms, with stream URLs |
|
| `find_radio_stations` | `radio` | Radio Browser stations by area and/or search terms, with stream URLs |
|
||||||
| `search_places` | `placeSearch` | Points of interest matching a query within an area (Google Places) |
|
| `search_places` | `placeSearch` | Points of interest matching a query within an area (Google Places) |
|
||||||
| `places_nearby` | `placeSearch` | Notable places around a point (Google Places) |
|
| `places_nearby` | `placeSearch` | Notable places around a place or point (Google Places) |
|
||||||
| `plan_route` | `routing` | Walking, driving or cycling route over OpenStreetMap, with a simplified path |
|
| `plan_route` | `routing` | Walking, driving or cycling route over OpenStreetMap, with a simplified path |
|
||||||
| `get_bike_share` | `bikeshare` | Live GBFS stations in an area, with bikes and docks available |
|
| `get_bike_share` | `bikeshare` | Live GBFS stations in an area, with bikes and docks available |
|
||||||
| `get_transit_vehicles` | `transit` | Live GTFS-Realtime vehicle positions in an area, optionally one route |
|
| `get_transit_vehicles` | `transit` | Live GTFS-Realtime vehicle positions in an area, optionally one route |
|
||||||
|
|||||||
@@ -200,8 +200,12 @@ export const getAircraftTrack = defineTool({
|
|||||||
requires: ['aircraft'],
|
requires: ['aircraft'],
|
||||||
async run(args, { services, signal }) {
|
async run(args, { services, signal }) {
|
||||||
const icao24 = args.icao24.toLowerCase();
|
const icao24 = args.icao24.toLowerCase();
|
||||||
const track = await feedFor(services, args.military).getTrack(icao24, {
|
// A track server answers 404 for an aircraft it has no positions for.
|
||||||
signal,
|
const track = await feedFor(services, args.military)
|
||||||
|
.getTrack(icao24, { signal })
|
||||||
|
.catch((error) => {
|
||||||
|
if (error?.status === 404) return { records: [] };
|
||||||
|
throw error;
|
||||||
});
|
});
|
||||||
const points = [...track.records].sort(
|
const points = [...track.records].sort(
|
||||||
(a, b) => a.observedAtMs - b.observedAtMs,
|
(a, b) => a.observedAtMs - b.observedAtMs,
|
||||||
@@ -284,13 +288,13 @@ export const getAircraftInfo = defineTool({
|
|||||||
if (icao24)
|
if (icao24)
|
||||||
parts.push(
|
parts.push(
|
||||||
aircraft
|
aircraft
|
||||||
? `${icao24} is ${aircraft.type_name || aircraft.type_code || 'an aircraft of unknown type'}${aircraft.registration ? ` (${aircraft.registration})` : ''}`
|
? `aircraft ${icao24} is ${aircraft.type_name || aircraft.type_code || 'an aircraft of unknown type'}${aircraft.registration ? ` (${aircraft.registration})` : ''}`
|
||||||
: `no type is known for ${icao24}`,
|
: `no type is known for ${icao24}`,
|
||||||
);
|
);
|
||||||
if (callsign)
|
if (callsign)
|
||||||
parts.push(
|
parts.push(
|
||||||
flight
|
flight
|
||||||
? `${callsign} flies ${flight.origin?.code || flight.origin?.name} to ${flight.destination?.code || flight.destination?.name}${flight.airline ? ` (${flight.airline})` : ''}`
|
? `flight ${callsign} flies ${flight.origin?.code || flight.origin?.name} to ${flight.destination?.code || flight.destination?.name}${flight.airline ? ` (${flight.airline})` : ''}`
|
||||||
: `no route is known for ${callsign}`,
|
: `no route is known for ${callsign}`,
|
||||||
);
|
);
|
||||||
const summary = parts.join('; ');
|
const summary = parts.join('; ');
|
||||||
|
|||||||
@@ -215,6 +215,30 @@ test('tracks are ordered and thinned to 200 points', async () => {
|
|||||||
(await empty.call('get_aircraft_track', { icao24: 'aaa111' })).summary,
|
(await empty.call('get_aircraft_track', { icao24: 'aaa111' })).summary,
|
||||||
'No recent track is available for aaa111.',
|
'No recent track is available for aaa111.',
|
||||||
);
|
);
|
||||||
|
const failing = (status) => ({
|
||||||
|
getSnapshot: async () => snapshot([]),
|
||||||
|
getTrack: async () => {
|
||||||
|
throw new LiveSourceError('unavailable', `OpenSky HTTP ${status}`, {
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const unknown = composeCatalog({
|
||||||
|
tools: coreTools,
|
||||||
|
services: { aircraft: failing(404) },
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
(await unknown.call('get_aircraft_track', { icao24: 'abcdef' })).summary,
|
||||||
|
'No recent track is available for abcdef.',
|
||||||
|
);
|
||||||
|
const down = composeCatalog({
|
||||||
|
tools: coreTools,
|
||||||
|
services: { aircraft: failing(502) },
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
down.call('get_aircraft_track', { icao24: 'abcdef' }),
|
||||||
|
(error) => error.code === 'unavailable',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('aircraft info combines type and route lookups', async () => {
|
test('aircraft info combines type and route lookups', async () => {
|
||||||
@@ -241,7 +265,7 @@ test('aircraft info combines type and route lookups', async () => {
|
|||||||
});
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
both.summary,
|
both.summary,
|
||||||
'Aaa111 is Boeing 737-800 (N12345); UAL123 flies SFO to EWR (United Airlines).',
|
'Aircraft aaa111 is Boeing 737-800 (N12345); flight UAL123 flies SFO to EWR (United Airlines).',
|
||||||
);
|
);
|
||||||
assert.equal(both.data.route.destination.name, 'Newark');
|
assert.equal(both.data.route.destination.name, 'Newark');
|
||||||
const unknown = await catalog.call('get_aircraft_info', { callsign: 'XYZ9' });
|
const unknown = await catalog.call('get_aircraft_info', { callsign: 'XYZ9' });
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const AWARENESS_RADIUS_KM = 250;
|
|||||||
const SECTIONS = [
|
const SECTIONS = [
|
||||||
{
|
{
|
||||||
key: 'weather',
|
key: 'weather',
|
||||||
|
label: 'Weather',
|
||||||
tool: getWeather,
|
tool: getWeather,
|
||||||
args: (area, center) => ({
|
args: (area, center) => ({
|
||||||
location: { lat: center.lat, lon: center.lon },
|
location: { lat: center.lat, lon: center.lon },
|
||||||
@@ -32,26 +33,31 @@ const SECTIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'earthquakes',
|
key: 'earthquakes',
|
||||||
|
label: 'Earthquakes',
|
||||||
tool: getEarthquakes,
|
tool: getEarthquakes,
|
||||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'fires',
|
key: 'fires',
|
||||||
|
label: 'Active fires',
|
||||||
tool: getActiveFires,
|
tool: getActiveFires,
|
||||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'aircraft',
|
key: 'aircraft',
|
||||||
|
label: 'Aircraft',
|
||||||
tool: aircraftInArea,
|
tool: aircraftInArea,
|
||||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'vessels',
|
key: 'vessels',
|
||||||
|
label: 'Ships',
|
||||||
tool: vesselsInArea,
|
tool: vesselsInArea,
|
||||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'cyclones',
|
key: 'cyclones',
|
||||||
|
label: 'Tropical cyclones',
|
||||||
tool: getCyclones,
|
tool: getCyclones,
|
||||||
args: (area) => ({ area: area.argument }),
|
args: (area) => ({ area: area.argument }),
|
||||||
},
|
},
|
||||||
@@ -181,9 +187,30 @@ export const getHudCaption = defineTool({
|
|||||||
},
|
},
|
||||||
requires: ['weather', 'summary'],
|
requires: ['weather', 'summary'],
|
||||||
async run(args, context) {
|
async run(args, context) {
|
||||||
const { area, center, lines } = await buildBrief(args, context);
|
const { area, center, brief, lines } = await buildBrief(args, context);
|
||||||
|
// The app's caption context: place labels and the layers that answered,
|
||||||
|
// with their feed state.
|
||||||
|
const answered = SECTIONS.filter(
|
||||||
|
({ key }) => brief[key] && !brief[key].unavailable,
|
||||||
|
);
|
||||||
|
const layers = answered.map(({ key, label }) => ({
|
||||||
|
id: key,
|
||||||
|
name: label,
|
||||||
|
feedState: 'nominal',
|
||||||
|
}));
|
||||||
const response = await context.services.summary.summarize(
|
const response = await context.services.summary.summarize(
|
||||||
{ location: area.label, center, observations: lines },
|
{
|
||||||
|
placeLabels: [area.label],
|
||||||
|
location: area.label,
|
||||||
|
center,
|
||||||
|
observations: lines,
|
||||||
|
enabledLayerLabels: layers.map((layer) => layer.name),
|
||||||
|
enabledLayers: layers,
|
||||||
|
feedProvenance: {
|
||||||
|
overall: layers.length ? 'nominal' : null,
|
||||||
|
layers,
|
||||||
|
},
|
||||||
|
},
|
||||||
{ signal: context.signal },
|
{ signal: context.signal },
|
||||||
);
|
);
|
||||||
const caption = response?.data?.summary;
|
const caption = response?.data?.summary;
|
||||||
|
|||||||
@@ -438,6 +438,9 @@ test('the HUD caption summarizes the brief through the summary service', async (
|
|||||||
);
|
);
|
||||||
assert.equal(result.summary, 'Overcast skies over Austin');
|
assert.equal(result.summary, 'Overcast skies over Austin');
|
||||||
assert.equal(sent[0].location, 'Austin, Texas');
|
assert.equal(sent[0].location, 'Austin, Texas');
|
||||||
|
assert.deepEqual(sent[0].placeLabels, ['Austin, Texas']);
|
||||||
|
assert.deepEqual(sent[0].enabledLayerLabels, ['Weather']);
|
||||||
|
assert.equal(sent[0].feedProvenance.overall, 'nominal');
|
||||||
assert.match(sent[0].observations[0], /^Weather at /);
|
assert.match(sent[0].observations[0], /^Weather at /);
|
||||||
const failing = {
|
const failing = {
|
||||||
summarize: async () => ({
|
summarize: async () => ({
|
||||||
|
|||||||
@@ -82,25 +82,25 @@ export const placesNearby = defineTool({
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
lat: { type: 'number', minimum: -90, maximum: 90 },
|
location: POINT_SCHEMA,
|
||||||
lon: { type: 'number', minimum: -180, maximum: 180 },
|
|
||||||
radius_m: { type: 'number', minimum: 10, maximum: 5000 },
|
radius_m: { type: 'number', minimum: 10, maximum: 5000 },
|
||||||
limit: LIMIT_SCHEMA,
|
limit: LIMIT_SCHEMA,
|
||||||
},
|
},
|
||||||
required: ['lat', 'lon'],
|
required: ['location'],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
requires: ['placeSearch'],
|
requires: ['placeSearch'],
|
||||||
async run(args, { services, signal }) {
|
async run(args, { services, signal }) {
|
||||||
|
const point = await resolvePoint(args.location, { services, signal });
|
||||||
const radiusM = Math.round(args.radius_m ?? 250);
|
const radiusM = Math.round(args.radius_m ?? 250);
|
||||||
const result = await services.placeSearch.nearby(
|
const result = await services.placeSearch.nearby(
|
||||||
{ latitude: args.lat, longitude: args.lon, radiusM },
|
{ latitude: point.lat, longitude: point.lon, radiusM },
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
if (!result.configured) throw new ToolError('unavailable', NOT_CONFIGURED);
|
if (!result.configured) throw new ToolError('unavailable', NOT_CONFIGURED);
|
||||||
const rows = result.places.map(placeRow);
|
const rows = result.places.map(placeRow);
|
||||||
return {
|
return {
|
||||||
summary: `${countNoun(rows.length, 'place')} within ${radiusM} m.`,
|
summary: `${countNoun(rows.length, 'place')} within ${radiusM} m of ${point.label}.`,
|
||||||
data: capRows(rows, args.limit),
|
data: capRows(rows, args.limit),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -93,11 +93,10 @@ test('nearby places use the given radius', async () => {
|
|||||||
services: { placeSearch: search },
|
services: { placeSearch: search },
|
||||||
});
|
});
|
||||||
const result = await catalog.call('places_nearby', {
|
const result = await catalog.call('places_nearby', {
|
||||||
lat: 30.27,
|
location: { lat: 30.27, lon: -97.74 },
|
||||||
lon: -97.74,
|
|
||||||
radius_m: 400,
|
radius_m: 400,
|
||||||
});
|
});
|
||||||
assert.equal(result.summary, '1 place within 400 m.');
|
assert.equal(result.summary, '1 place within 400 m of 30.2700, -97.7400.');
|
||||||
assert.deepEqual(search.calls[0], {
|
assert.deepEqual(search.calls[0], {
|
||||||
latitude: 30.27,
|
latitude: 30.27,
|
||||||
longitude: -97.74,
|
longitude: -97.74,
|
||||||
|
|||||||
+26
-26
@@ -6,6 +6,7 @@ import {
|
|||||||
lookAnglesAt,
|
lookAnglesAt,
|
||||||
} from '../../data/satellitePass.js';
|
} from '../../data/satellitePass.js';
|
||||||
import { parseTleText, tleCatalogNumber } from '../../sources/tle.js';
|
import { parseTleText, tleCatalogNumber } from '../../sources/tle.js';
|
||||||
|
import { POINT_SCHEMA, resolvePoint } from '../area.js';
|
||||||
import { defineTool, ToolError } from '../catalog.js';
|
import { defineTool, ToolError } from '../catalog.js';
|
||||||
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
|
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
|
||||||
|
|
||||||
@@ -83,20 +84,6 @@ const COMPASS = [
|
|||||||
'NW',
|
'NW',
|
||||||
'NNW',
|
'NNW',
|
||||||
];
|
];
|
||||||
const OBSERVER = {
|
|
||||||
lat: {
|
|
||||||
type: 'number',
|
|
||||||
minimum: -90,
|
|
||||||
maximum: 90,
|
|
||||||
description: 'Observer latitude.',
|
|
||||||
},
|
|
||||||
lon: {
|
|
||||||
type: 'number',
|
|
||||||
minimum: -180,
|
|
||||||
maximum: 180,
|
|
||||||
description: 'Observer longitude.',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const GROUP_SCHEMA = {
|
const GROUP_SCHEMA = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: GROUPS,
|
enum: GROUPS,
|
||||||
@@ -154,7 +141,7 @@ export const nextSatellitePass = defineTool({
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
...OBSERVER,
|
location: POINT_SCHEMA,
|
||||||
satellite: {
|
satellite: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
@@ -170,11 +157,12 @@ export const nextSatellitePass = defineTool({
|
|||||||
},
|
},
|
||||||
hours: { type: 'number', minimum: 1, maximum: 72 },
|
hours: { type: 'number', minimum: 1, maximum: 72 },
|
||||||
},
|
},
|
||||||
required: ['lat', 'lon'],
|
required: ['location'],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
requires: ['satellites'],
|
requires: ['satellites'],
|
||||||
async run(args, { services, signal }) {
|
async run(args, { services, signal }) {
|
||||||
|
const point = await resolvePoint(args.location, { services, signal });
|
||||||
const group = args.group ?? 'stations';
|
const group = args.group ?? 'stations';
|
||||||
const entries = await readCatalog(services, group, signal);
|
const entries = await readCatalog(services, group, signal);
|
||||||
const satellite = matchSatellite(entries, args.satellite);
|
const satellite = matchSatellite(entries, args.satellite);
|
||||||
@@ -186,8 +174,8 @@ export const nextSatellitePass = defineTool({
|
|||||||
const hours = args.hours ?? 24;
|
const hours = args.hours ?? 24;
|
||||||
const pass = findNextSatellitePass({
|
const pass = findNextSatellitePass({
|
||||||
satrec: satellite.satrec,
|
satrec: satellite.satrec,
|
||||||
latDeg: args.lat,
|
latDeg: point.lat,
|
||||||
lonDeg: args.lon,
|
lonDeg: point.lon,
|
||||||
fromMs: nowMs(services),
|
fromMs: nowMs(services),
|
||||||
minElevDeg: args.min_elevation_deg ?? 10,
|
minElevDeg: args.min_elevation_deg ?? 10,
|
||||||
horizonHours: hours,
|
horizonHours: hours,
|
||||||
@@ -196,15 +184,21 @@ export const nextSatellitePass = defineTool({
|
|||||||
const name = satellite.name;
|
const name = satellite.name;
|
||||||
if (!pass)
|
if (!pass)
|
||||||
return {
|
return {
|
||||||
summary: `${name} has no ${args.visible_only ? 'visible ' : ''}pass over this location in the next ${hours} hours.`,
|
summary: `${name} has no ${args.visible_only ? 'visible ' : ''}pass over ${point.label} in the next ${hours} hours.`,
|
||||||
data: { satellite: name, norad: satellite.norad, pass: null },
|
data: {
|
||||||
|
location: point,
|
||||||
|
satellite: name,
|
||||||
|
norad: satellite.norad,
|
||||||
|
pass: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
summary:
|
summary:
|
||||||
`${name} next rises ${isoTime(pass.riseMs)} in the ${compass(pass.riseAzDeg)}, ` +
|
`${name} next rises over ${point.label} at ${isoTime(pass.riseMs)} in the ${compass(pass.riseAzDeg)}, ` +
|
||||||
`peaking at ${Math.round(pass.maxElevDeg)}°` +
|
`peaking at ${Math.round(pass.maxElevDeg)}°` +
|
||||||
`${pass.visible ? '; visible to the naked eye' : ''}.`,
|
`${pass.visible ? '; visible to the naked eye' : ''}.`,
|
||||||
data: {
|
data: {
|
||||||
|
location: point,
|
||||||
satellite: name,
|
satellite: name,
|
||||||
norad: satellite.norad,
|
norad: satellite.norad,
|
||||||
pass: {
|
pass: {
|
||||||
@@ -230,22 +224,23 @@ export const satellitesOverhead = defineTool({
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
...OBSERVER,
|
location: POINT_SCHEMA,
|
||||||
group: GROUP_SCHEMA,
|
group: GROUP_SCHEMA,
|
||||||
min_elevation_deg: { type: 'number', minimum: 0, maximum: 90 },
|
min_elevation_deg: { type: 'number', minimum: 0, maximum: 90 },
|
||||||
limit: LIMIT_SCHEMA,
|
limit: LIMIT_SCHEMA,
|
||||||
},
|
},
|
||||||
required: ['lat', 'lon'],
|
required: ['location'],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
requires: ['satellites'],
|
requires: ['satellites'],
|
||||||
async run(args, { services, signal }) {
|
async run(args, { services, signal }) {
|
||||||
|
const point = await resolvePoint(args.location, { services, signal });
|
||||||
const group = args.group ?? 'stations';
|
const group = args.group ?? 'stations';
|
||||||
const minimum = args.min_elevation_deg ?? 10;
|
const minimum = args.min_elevation_deg ?? 10;
|
||||||
const at = nowMs(services);
|
const at = nowMs(services);
|
||||||
const rows = (await readCatalog(services, group, signal))
|
const rows = (await readCatalog(services, group, signal))
|
||||||
.flatMap((entry) => {
|
.flatMap((entry) => {
|
||||||
const look = lookAnglesAt(entry.satrec, at, args.lat, args.lon);
|
const look = lookAnglesAt(entry.satrec, at, point.lat, point.lon);
|
||||||
return look && look.elevDeg >= minimum
|
return look && look.elevDeg >= minimum
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -260,8 +255,13 @@ export const satellitesOverhead = defineTool({
|
|||||||
})
|
})
|
||||||
.sort((a, b) => b.elevation_deg - a.elevation_deg);
|
.sort((a, b) => b.elevation_deg - a.elevation_deg);
|
||||||
return {
|
return {
|
||||||
summary: `${countNoun(rows.length, 'satellite')} from the ${group} group ${rows.length === 1 ? 'is' : 'are'} at least ${minimum}° above this location at ${isoTime(at)}.`,
|
summary: `${countNoun(rows.length, 'satellite')} from the ${group} group ${rows.length === 1 ? 'is' : 'are'} at least ${minimum}° above ${point.label} at ${isoTime(at)}.`,
|
||||||
data: { ...capRows(rows, args.limit), group, at: isoTime(at) },
|
data: {
|
||||||
|
...capRows(rows, args.limit),
|
||||||
|
location: point,
|
||||||
|
group,
|
||||||
|
at: isoTime(at),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -36,8 +36,7 @@ test('the next pass defaults to the ISS and reports times, peak and direction',
|
|||||||
services: { satellites: satellites(), clock: CLOCK },
|
services: { satellites: satellites(), clock: CLOCK },
|
||||||
});
|
});
|
||||||
const result = await catalog.call('next_satellite_pass', {
|
const result = await catalog.call('next_satellite_pass', {
|
||||||
lat: 40.7,
|
location: { lat: 40.7, lon: -74 },
|
||||||
lon: -74,
|
|
||||||
});
|
});
|
||||||
const { pass } = result.data;
|
const { pass } = result.data;
|
||||||
assert.equal(result.data.satellite, 'ISS (ZARYA)');
|
assert.equal(result.data.satellite, 'ISS (ZARYA)');
|
||||||
@@ -50,7 +49,7 @@ test('the next pass defaults to the ISS and reports times, peak and direction',
|
|||||||
assert.match(
|
assert.match(
|
||||||
result.summary,
|
result.summary,
|
||||||
new RegExp(
|
new RegExp(
|
||||||
`^ISS \\(ZARYA\\) next rises ${pass.rise} in the ${pass.rise_direction}, peaking at \\d+°`,
|
`^ISS \\(ZARYA\\) next rises over 40.7000, -74.0000 at ${pass.rise} in the ${pass.rise_direction}, peaking at \\d+°`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -62,14 +61,12 @@ test('satellites are matched by name or catalog number within the group', async
|
|||||||
services: { satellites: source, clock: CLOCK },
|
services: { satellites: source, clock: CLOCK },
|
||||||
});
|
});
|
||||||
const byName = await catalog.call('next_satellite_pass', {
|
const byName = await catalog.call('next_satellite_pass', {
|
||||||
lat: 0,
|
location: { lat: 0, lon: 0 },
|
||||||
lon: 0,
|
|
||||||
satellite: 'tianhe',
|
satellite: 'tianhe',
|
||||||
});
|
});
|
||||||
assert.equal(byName.data.norad, 48274);
|
assert.equal(byName.data.norad, 48274);
|
||||||
const byNumber = await catalog.call('next_satellite_pass', {
|
const byNumber = await catalog.call('next_satellite_pass', {
|
||||||
lat: 0,
|
location: { lat: 0, lon: 0 },
|
||||||
lon: 0,
|
|
||||||
satellite: '48274',
|
satellite: '48274',
|
||||||
group: 'visual',
|
group: 'visual',
|
||||||
});
|
});
|
||||||
@@ -77,14 +74,16 @@ test('satellites are matched by name or catalog number within the group', async
|
|||||||
assert.deepEqual(source.calls, ['stations', 'visual']);
|
assert.deepEqual(source.calls, ['stations', 'visual']);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
catalog.call('next_satellite_pass', {
|
catalog.call('next_satellite_pass', {
|
||||||
lat: 0,
|
location: { lat: 0, lon: 0 },
|
||||||
lon: 0,
|
|
||||||
satellite: 'Hubble',
|
satellite: 'Hubble',
|
||||||
}),
|
}),
|
||||||
/No satellite matching "Hubble" is in the stations group/,
|
/No satellite matching "Hubble" is in the stations group/,
|
||||||
);
|
);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
catalog.call('next_satellite_pass', { lat: 0, lon: 0, group: 'weather' }),
|
catalog.call('next_satellite_pass', {
|
||||||
|
location: { lat: 0, lon: 0 },
|
||||||
|
group: 'weather',
|
||||||
|
}),
|
||||||
/must be one of/,
|
/must be one of/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -96,21 +95,20 @@ test('a pass that never comes is a normal answer, and catalog failures are unava
|
|||||||
});
|
});
|
||||||
// The ISS orbit is inclined 51.6°, so it never rises 10° above the pole.
|
// The ISS orbit is inclined 51.6°, so it never rises 10° above the pole.
|
||||||
const none = await catalog.call('next_satellite_pass', {
|
const none = await catalog.call('next_satellite_pass', {
|
||||||
lat: 89.9,
|
location: { lat: 89.9, lon: 0 },
|
||||||
lon: 0,
|
|
||||||
hours: 6,
|
hours: 6,
|
||||||
});
|
});
|
||||||
assert.equal(none.data.pass, null);
|
assert.equal(none.data.pass, null);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
none.summary,
|
none.summary,
|
||||||
'ISS (ZARYA) has no pass over this location in the next 6 hours.',
|
'ISS (ZARYA) has no pass over 89.9000, 0.0000 in the next 6 hours.',
|
||||||
);
|
);
|
||||||
const down = composeCatalog({
|
const down = composeCatalog({
|
||||||
tools: coreTools,
|
tools: coreTools,
|
||||||
services: { satellites: satellites('', false), clock: CLOCK },
|
services: { satellites: satellites('', false), clock: CLOCK },
|
||||||
});
|
});
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
down.call('next_satellite_pass', { lat: 0, lon: 0 }),
|
down.call('next_satellite_pass', { location: { lat: 0, lon: 0 } }),
|
||||||
(error) => error.code === 'unavailable' && /HTTP 503/.test(error.message),
|
(error) => error.code === 'unavailable' && /HTTP 503/.test(error.message),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -120,7 +118,9 @@ test('satellites overhead are those above the elevation, highest first', async (
|
|||||||
const services = { satellites: source, clock: CLOCK };
|
const services = { satellites: source, clock: CLOCK };
|
||||||
const catalog = composeCatalog({ tools: coreTools, services });
|
const catalog = composeCatalog({ tools: coreTools, services });
|
||||||
const pass = (
|
const pass = (
|
||||||
await catalog.call('next_satellite_pass', { lat: 40.7, lon: -74 })
|
await catalog.call('next_satellite_pass', {
|
||||||
|
location: { lat: 40.7, lon: -74 },
|
||||||
|
})
|
||||||
).data.pass;
|
).data.pass;
|
||||||
const atPeak = {
|
const atPeak = {
|
||||||
satellites: source,
|
satellites: source,
|
||||||
@@ -130,8 +130,7 @@ test('satellites overhead are those above the elevation, highest first', async (
|
|||||||
tools: coreTools,
|
tools: coreTools,
|
||||||
services: atPeak,
|
services: atPeak,
|
||||||
}).call('satellites_overhead', {
|
}).call('satellites_overhead', {
|
||||||
lat: 40.7,
|
location: { lat: 40.7, lon: -74 },
|
||||||
lon: -74,
|
|
||||||
});
|
});
|
||||||
assert.equal(overhead.data.rows[0].name, 'ISS (ZARYA)');
|
assert.equal(overhead.data.rows[0].name, 'ISS (ZARYA)');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
@@ -142,8 +141,7 @@ test('satellites overhead are those above the elevation, highest first', async (
|
|||||||
tools: coreTools,
|
tools: coreTools,
|
||||||
services: atPeak,
|
services: atPeak,
|
||||||
}).call('satellites_overhead', {
|
}).call('satellites_overhead', {
|
||||||
lat: 40.7,
|
location: { lat: 40.7, lon: -74 },
|
||||||
lon: -74,
|
|
||||||
min_elevation_deg: 90,
|
min_elevation_deg: 90,
|
||||||
});
|
});
|
||||||
assert.equal(strict.data.total, 0);
|
assert.equal(strict.data.total, 0);
|
||||||
|
|||||||
Reference in new issue
Block a user