mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 23:32:20 +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
|
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
|
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
|
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
|
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
|
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;
|
destination for ships; the ten largest groups, then other and unknown counts;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
numericSummary,
|
numericSummary,
|
||||||
thinEvenly,
|
thinEvenly,
|
||||||
} from '../results.js';
|
} from '../results.js';
|
||||||
|
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
|
||||||
|
|
||||||
const ICAO24_SCHEMA = Object.freeze({
|
const ICAO24_SCHEMA = Object.freeze({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -75,7 +76,7 @@ function aircraftRow(record, from) {
|
|||||||
altitude_m: round(record.baroAltitudeM ?? record.ellipsoidAltitudeM, 0),
|
altitude_m: round(record.baroAltitudeM ?? record.ellipsoidAltitudeM, 0),
|
||||||
on_ground: record.onGround === true,
|
on_ground: record.onGround === true,
|
||||||
speed_mps: round(record.speedMps, 1),
|
speed_mps: round(record.speedMps, 1),
|
||||||
course_deg: round(record.courseDeg, 0),
|
course_deg: roundHeadingDegrees(record.courseDeg),
|
||||||
vertical_rate_mps: round(record.verticalRateMps, 1),
|
vertical_rate_mps: round(record.verticalRateMps, 1),
|
||||||
type_code: record.typeCode || null,
|
type_code: record.typeCode || null,
|
||||||
registration: record.registration || 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 () => {
|
test('aircraft are found by exactly one identifier across feeds', async () => {
|
||||||
const catalog = composeCatalog({
|
const catalog = composeCatalog({
|
||||||
tools: coreTools,
|
tools: coreTools,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
numericSummary,
|
numericSummary,
|
||||||
thinEvenly,
|
thinEvenly,
|
||||||
} from '../results.js';
|
} from '../results.js';
|
||||||
|
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
|
||||||
import { normalizeVesselType } from '../../data/vesselTypes.js';
|
import { normalizeVesselType } from '../../data/vesselTypes.js';
|
||||||
|
|
||||||
const MMSI_SCHEMA = Object.freeze({
|
const MMSI_SCHEMA = Object.freeze({
|
||||||
@@ -57,8 +58,8 @@ function vesselRow(record, from) {
|
|||||||
lat: round(record.latitude, 5),
|
lat: round(record.latitude, 5),
|
||||||
lon: round(record.longitude, 5),
|
lon: round(record.longitude, 5),
|
||||||
speed_mps: round(record.speedMps, 1),
|
speed_mps: round(record.speedMps, 1),
|
||||||
course_deg: round(record.courseDeg, 0),
|
course_deg: roundHeadingDegrees(record.courseDeg),
|
||||||
heading_deg: round(record.headingDeg, 0),
|
heading_deg: roundHeadingDegrees(record.headingDeg),
|
||||||
observed_at: isoTime(record.observedAtMs),
|
observed_at: isoTime(record.observedAtMs),
|
||||||
...(from ? { distance_km: round(distanceKm(from, point), 1) } : {}),
|
...(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 () => {
|
test('a destination with no letters counts as unknown', async () => {
|
||||||
const catalog = composeCatalog({
|
const catalog = composeCatalog({
|
||||||
tools: coreTools,
|
tools: coreTools,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
resolveArea,
|
resolveArea,
|
||||||
} from '../area.js';
|
} from '../area.js';
|
||||||
import { LIMIT_SCHEMA, capRows, countNoun, toBase64 } from '../results.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 FRAME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
||||||
const MAX_FRAME_BYTES = 3 * 1024 * 1024;
|
const MAX_FRAME_BYTES = 3 * 1024 * 1024;
|
||||||
@@ -85,7 +86,7 @@ export const findCctvCameras = defineTool({
|
|||||||
provider: text(camera.provider),
|
provider: text(camera.provider),
|
||||||
lat: round(camera.lat, 6),
|
lat: round(camera.lat, 6),
|
||||||
lon: round(camera.lon, 6),
|
lon: round(camera.lon, 6),
|
||||||
heading_deg: round(camera.headingDeg, 0),
|
heading_deg: roundHeadingDegrees(camera.headingDeg),
|
||||||
feed_type: text(camera.feedType),
|
feed_type: text(camera.feedType),
|
||||||
credit: text(camera.credit),
|
credit: text(camera.credit),
|
||||||
distance_km: round(distanceKm(center, camera), 2),
|
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);
|
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 () => {
|
test('a snapshot returns the frame as an image with its credit', async () => {
|
||||||
const source = cctv();
|
const source = cctv();
|
||||||
const catalog = composeCatalog({
|
const catalog = composeCatalog({
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
resolveArea,
|
resolveArea,
|
||||||
} from '../area.js';
|
} from '../area.js';
|
||||||
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
|
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
|
||||||
|
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
|
||||||
|
|
||||||
const MAX_SYSTEMS = 3;
|
const MAX_SYSTEMS = 3;
|
||||||
|
|
||||||
@@ -244,7 +245,7 @@ export const getTransitVehicles = defineTool({
|
|||||||
trip_id: vehicle.tripId ?? null,
|
trip_id: vehicle.tripId ?? null,
|
||||||
lat: vehicle.lat,
|
lat: vehicle.lat,
|
||||||
lon: vehicle.lon,
|
lon: vehicle.lon,
|
||||||
bearing_deg: round(vehicle.bearing, 0),
|
bearing_deg: roundHeadingDegrees(vehicle.bearing),
|
||||||
speed_mps: round(vehicle.speedMps, 1),
|
speed_mps: round(vehicle.speedMps, 1),
|
||||||
status: vehicle.status ?? null,
|
status: vehicle.status ?? null,
|
||||||
occupancy: vehicle.occupancy ?? 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');
|
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 () => {
|
test('transit reports uncovered areas and unavailable feeds', async () => {
|
||||||
const catalog = composeCatalog({
|
const catalog = composeCatalog({
|
||||||
tools: coreTools,
|
tools: coreTools,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { parseTleText, tleCatalogNumber } from '../../sources/tle.js';
|
|||||||
import { POINT_SCHEMA, resolvePoint } from '../area.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';
|
||||||
|
import { roundHeadingDegrees } from '../../data/headingDegrees.js';
|
||||||
|
|
||||||
const text = (value) => (typeof value === 'string' && value ? value : null);
|
const text = (value) => (typeof value === 'string' && value ? value : null);
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ export const nextSatellitePass = defineTool({
|
|||||||
peak: isoTime(pass.maxElevMs),
|
peak: isoTime(pass.maxElevMs),
|
||||||
set: isoTime(pass.setMs),
|
set: isoTime(pass.setMs),
|
||||||
max_elevation_deg: round(pass.maxElevDeg, 1),
|
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),
|
rise_direction: compass(pass.riseAzDeg),
|
||||||
visible: pass.visible,
|
visible: pass.visible,
|
||||||
},
|
},
|
||||||
@@ -283,7 +284,7 @@ export const satellitesOverhead = defineTool({
|
|||||||
name: entry.name,
|
name: entry.name,
|
||||||
norad: entry.norad,
|
norad: entry.norad,
|
||||||
elevation_deg: round(look.elevDeg, 1),
|
elevation_deg: round(look.elevDeg, 1),
|
||||||
azimuth_deg: round(look.azDeg, 0),
|
azimuth_deg: roundHeadingDegrees(look.azDeg),
|
||||||
direction: compass(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 () => {
|
test('stale orbit and launch data are reported', async () => {
|
||||||
const stale = {
|
const stale = {
|
||||||
readGroup: async () => ({ ok: true, status: 200, text: TLE, stale: true }),
|
readGroup: async () => ({ ok: true, status: 200, text: TLE, stale: true }),
|
||||||
|
|||||||
Reference in new issue
Block a user