mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-09 21:02:19 +00:00
fix(tools): wrap whole-degree headings at 360 to 0
The query tools rounded courses, headings, bearings and azimuths with Number(value.toFixed(0)) and no wrap, so a direction of 359.5 degrees or more came back as 360: find_vessel with course 359.7 and heading 359.6 returned course_deg 360 and heading_deg 360. Round them through the shared roundHeadingDegrees helper the layers and UI already use (src/data/headingDegrees.js), which wraps into 0 to 359: course_deg and heading_deg (vessels), course_deg (aircraft), bearing_deg (transit), heading_deg (cameras), and rise_azimuth_deg and azimuth_deg (satellites). Other rounding is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f3adccd01a
commit
ae55963a0e
11 files changed
+97
-8
No files matched your search
@@ -204,7 +204,10 @@ perimeters, terrain height, military installations and map features, plus a
|
||||
combined situation brief, military awareness around a point, the app's heads-up display caption, and a link that
|
||||
opens the app over an area. Tools reuse the layers' portable source factories, take a
|
||||
shared `area` argument (place name, bounding box, or point and radius) and cap
|
||||
lists at 25 rows by default. `aircraft_in_area` and `vessels_in_area` can also
|
||||
lists at 25 rows by default. Vessel `course_deg` and `heading_deg`, aircraft
|
||||
`course_deg`, transit `bearing_deg`, camera `heading_deg` and satellite
|
||||
`rise_azimuth_deg` and `azimuth_deg` are whole degrees from 0 to 359; a
|
||||
direction of 359.5° or more reads 0 rather than 360. `aircraft_in_area` and `vessels_in_area` can also
|
||||
count every match by a field (`group_by`: airline, read from the ICAO
|
||||
designator in the callsign, or operator, country or type for aircraft; type or
|
||||
destination for ships; the ten largest groups, then other and unknown counts;
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
numericSummary,
|
||||
thinEvenly,
|
||||
} from '../results.js';
|
||||
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
|
||||
|
||||
const ICAO24_SCHEMA = Object.freeze({
|
||||
type: 'string',
|
||||
@@ -75,7 +76,7 @@ function aircraftRow(record, from) {
|
||||
altitude_m: round(record.baroAltitudeM ?? record.ellipsoidAltitudeM, 0),
|
||||
on_ground: record.onGround === true,
|
||||
speed_mps: round(record.speedMps, 1),
|
||||
course_deg: round(record.courseDeg, 0),
|
||||
course_deg: roundHeadingDegrees(record.courseDeg),
|
||||
vertical_rate_mps: round(record.verticalRateMps, 1),
|
||||
type_code: record.typeCode || null,
|
||||
registration: record.registration || null,
|
||||
|
||||
@@ -146,6 +146,19 @@ test('the military feed is used on request and reported when missing', async ()
|
||||
);
|
||||
});
|
||||
|
||||
test('an aircraft course that rounds to 360 reads as 0', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
aircraft: feed([
|
||||
record('ddd444', 37.6, -122.3, { callsign: 'N360', courseDeg: 359.6 }),
|
||||
]),
|
||||
},
|
||||
});
|
||||
const result = await catalog.call('find_aircraft', { callsign: 'N360' });
|
||||
assert.equal(result.data.rows[0].course_deg, 0);
|
||||
});
|
||||
|
||||
test('aircraft are found by exactly one identifier across feeds', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
numericSummary,
|
||||
thinEvenly,
|
||||
} from '../results.js';
|
||||
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
|
||||
import { normalizeVesselType } from '../../data/vesselTypes.js';
|
||||
|
||||
const MMSI_SCHEMA = Object.freeze({
|
||||
@@ -57,8 +58,8 @@ function vesselRow(record, from) {
|
||||
lat: round(record.latitude, 5),
|
||||
lon: round(record.longitude, 5),
|
||||
speed_mps: round(record.speedMps, 1),
|
||||
course_deg: round(record.courseDeg, 0),
|
||||
heading_deg: round(record.headingDeg, 0),
|
||||
course_deg: roundHeadingDegrees(record.courseDeg),
|
||||
heading_deg: roundHeadingDegrees(record.headingDeg),
|
||||
observed_at: isoTime(record.observedAtMs),
|
||||
...(from ? { distance_km: round(distanceKm(from, point), 1) } : {}),
|
||||
};
|
||||
|
||||
@@ -351,6 +351,25 @@ test('find_vessel answers carry normalized types and destinations', async () =>
|
||||
);
|
||||
});
|
||||
|
||||
test('a vessel course or heading that rounds to 360 reads as 0', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
vessels: vessels({
|
||||
records: [
|
||||
vessel('366000003', 37.78, -122.4, {
|
||||
courseDeg: 359.7,
|
||||
headingDeg: 359.6,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
const [row] = (await catalog.call('find_vessel', { mmsi: '366000003' })).data
|
||||
.rows;
|
||||
assert.deepEqual([row.course_deg, row.heading_deg], [0, 0]);
|
||||
});
|
||||
|
||||
test('a destination with no letters counts as unknown', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
resolveArea,
|
||||
} from '../area.js';
|
||||
import { LIMIT_SCHEMA, capRows, countNoun, toBase64 } from '../results.js';
|
||||
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
|
||||
|
||||
const FRAME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
||||
const MAX_FRAME_BYTES = 3 * 1024 * 1024;
|
||||
@@ -85,7 +86,7 @@ export const findCctvCameras = defineTool({
|
||||
provider: text(camera.provider),
|
||||
lat: round(camera.lat, 6),
|
||||
lon: round(camera.lon, 6),
|
||||
heading_deg: round(camera.headingDeg, 0),
|
||||
heading_deg: roundHeadingDegrees(camera.headingDeg),
|
||||
feed_type: text(camera.feedType),
|
||||
credit: text(camera.credit),
|
||||
distance_km: round(distanceKm(center, camera), 2),
|
||||
|
||||
@@ -77,6 +77,20 @@ test('cameras in an area are listed nearest first', async () => {
|
||||
assert.equal(result.data.rows[1].credit, null);
|
||||
});
|
||||
|
||||
test('a camera heading that rounds to 360 reads as 0', async () => {
|
||||
const source = {
|
||||
async getCatalog() {
|
||||
return { sources: [{ ...cameras[0], headingDeg: 359.6 }] };
|
||||
},
|
||||
};
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { cctv: source },
|
||||
});
|
||||
const result = await catalog.call('find_cctv_cameras', { area: downtown });
|
||||
assert.equal(result.data.rows[0].heading_deg, 0);
|
||||
});
|
||||
|
||||
test('a snapshot returns the frame as an image with its credit', async () => {
|
||||
const source = cctv();
|
||||
const catalog = composeCatalog({
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
resolveArea,
|
||||
} from '../area.js';
|
||||
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
|
||||
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
|
||||
|
||||
const MAX_SYSTEMS = 3;
|
||||
|
||||
@@ -244,7 +245,7 @@ export const getTransitVehicles = defineTool({
|
||||
trip_id: vehicle.tripId ?? null,
|
||||
lat: vehicle.lat,
|
||||
lon: vehicle.lon,
|
||||
bearing_deg: round(vehicle.bearing, 0),
|
||||
bearing_deg: roundHeadingDegrees(vehicle.bearing),
|
||||
speed_mps: round(vehicle.speedMps, 1),
|
||||
status: vehicle.status ?? null,
|
||||
occupancy: vehicle.occupancy ?? null,
|
||||
|
||||
@@ -311,6 +311,23 @@ test('transit vehicles come from nearby feeds and can be limited to a route', as
|
||||
assert.equal(rapid.data.rows[0].id, 'v2');
|
||||
});
|
||||
|
||||
test('a transit bearing that rounds to 360 reads as 0', async () => {
|
||||
const north = [{ ...vehicles[0], bearing: 359.6 }];
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
transit: transit({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ vehicles: north }),
|
||||
}),
|
||||
clock,
|
||||
},
|
||||
});
|
||||
const result = await catalog.call('get_transit_vehicles', { area: downtown });
|
||||
assert.equal(result.data.rows[0].bearing_deg, 0);
|
||||
});
|
||||
|
||||
test('transit reports uncovered areas and unavailable feeds', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { parseTleText, tleCatalogNumber } from '../../sources/tle.js';
|
||||
import { POINT_SCHEMA, resolvePoint } from '../area.js';
|
||||
import { defineTool, ToolError } from '../catalog.js';
|
||||
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
|
||||
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
|
||||
|
||||
const text = (value) => (typeof value === 'string' && value ? value : null);
|
||||
|
||||
@@ -241,7 +242,7 @@ export const nextSatellitePass = defineTool({
|
||||
peak: isoTime(pass.maxElevMs),
|
||||
set: isoTime(pass.setMs),
|
||||
max_elevation_deg: round(pass.maxElevDeg, 1),
|
||||
rise_azimuth_deg: round(pass.riseAzDeg, 0),
|
||||
rise_azimuth_deg: roundHeadingDegrees(pass.riseAzDeg),
|
||||
rise_direction: compass(pass.riseAzDeg),
|
||||
visible: pass.visible,
|
||||
},
|
||||
@@ -283,7 +284,7 @@ export const satellitesOverhead = defineTool({
|
||||
name: entry.name,
|
||||
norad: entry.norad,
|
||||
elevation_deg: round(look.elevDeg, 1),
|
||||
azimuth_deg: round(look.azDeg, 0),
|
||||
azimuth_deg: roundHeadingDegrees(look.azDeg),
|
||||
direction: compass(look.azDeg),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -296,6 +296,24 @@ test('satellites overhead are those above the elevation, highest first', async (
|
||||
);
|
||||
});
|
||||
|
||||
test('satellite azimuths that round to 360 read as 0', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { satellites: satellites(), clock: CLOCK },
|
||||
});
|
||||
// From these points the ISS rises at 359.7° and stands at 359.8° at CLOCK.
|
||||
const pass = await catalog.call('next_satellite_pass', {
|
||||
location: { lat: 10, lon: 30 },
|
||||
});
|
||||
assert.equal(pass.data.pass.rise_azimuth_deg, 0);
|
||||
assert.equal(pass.data.pass.rise_direction, 'N');
|
||||
const overhead = await catalog.call('satellites_overhead', {
|
||||
location: { lat: 39.25, lon: 65.75 },
|
||||
});
|
||||
assert.equal(overhead.data.rows[0].name, 'ISS (ZARYA)');
|
||||
assert.equal(overhead.data.rows[0].azimuth_deg, 0);
|
||||
});
|
||||
|
||||
test('stale orbit and launch data are reported', async () => {
|
||||
const stale = {
|
||||
readGroup: async () => ({ ok: true, status: 200, text: TLE, stale: true }),
|
||||
|
||||
Reference in new issue
Block a user