feat(satellites): find the satellites nearest a position

getNearestPositions makes one bounded pass over the shown points, keeps the
nearest few in a short sorted list and reads latitude and longitude for
those alone, leaving out dense extras, asked-for ids and classes such as
debris. A caller that wants the nearest handful no longer builds and sorts
an entry for every satellite.
This commit is contained in:
Denis Groshev committed 2026-11-09 01:56:30 +01:00
1 parent 569c7cfb1b
commit fad62a4ba8
2 files changed
+129

No files matched your search

+62
View File
@@ -371,6 +371,68 @@ export function createControls({ state: layerState, services, parts, source }) {
return result;
},
/**
* The shown satellites nearest a position, nearest first, from per-point
* state (no SGP4 re-propagation): a bounded pass that keeps only the
* nearest `maxCount`, and reads latitude and longitude for those alone.
* Dense extras are left out, as in `getAllPositions`.
* @param {Cesium.Cartesian3} center Earth-fixed position.
* @param {number} [maxCount=8] Most entries to return.
* @param {{excludeIds?: Array<number|string>,
* excludeClasses?: Array<string>}} [options] Satellites, and classes
* (e.g. 'debris'), to leave out.
* @returns {Array<{ id: number, label: string, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number, satelliteClass: string|null, distanceM: number }>}
*/
getNearestPositions(
center,
maxCount = 8,
{ excludeIds = [], excludeClasses = [] } = {},
) {
const nearest = [];
if (!center || !layerState._points || layerState._points.size === 0)
return nearest;
const cap =
Number.isFinite(maxCount) && maxCount > 0 ? Math.floor(maxCount) : 8;
const excluded = new Set(excludeIds.map(String));
const classes = new Set(excludeClasses);
const dense = layerState._denseIds.length > 0;
for (const [noradId, point] of layerState._points) {
const p = point.position;
if (!p || excluded.has(String(noradId))) continue;
if (!_classVisible(noradId)) continue;
const sat = layerState._catalog.get(noradId);
if (dense && sat?.group === 'dense') continue;
if (classes.size && classes.has(sat?.satelliteClass)) continue;
const dx = p.x - center.x;
const dy = p.y - center.y;
const dz = p.z - center.z;
const distanceSq = dx * dx + dy * dy + dz * dz;
if (nearest.length === cap && distanceSq >= nearest[cap - 1].distanceSq)
continue;
// Insertion into a short sorted list: no copy or sort of the rest.
let at = nearest.length;
while (at > 0 && nearest[at - 1].distanceSq > distanceSq) at -= 1;
nearest.splice(at, 0, { noradId, point, sat, distanceSq });
if (nearest.length > cap) nearest.pop();
}
const result = [];
for (const { noradId, point, sat, distanceSq } of nearest) {
const carto = Cesium.Cartographic.fromCartesian(point.position);
if (!carto) continue;
result.push({
id: noradId,
label: sat ? sat.name.trim() : String(noradId),
position: point.position,
latitude: Cesium.Math.toDegrees(carto.latitude),
longitude: Cesium.Math.toDegrees(carto.longitude),
altitudeM: carto.height,
satelliteClass: sat?.satelliteClass ?? null,
distanceM: Math.sqrt(distanceSq),
});
}
return result;
},
/**
* Track a satellite by NORAD id (camera follow + orbit path + highlight),
* same path as clicking its point.
@@ -0,0 +1,67 @@
// src/layers/satellites/nearestPositions.test.mjs — the satellites nearest a
// position: bounded to the nearest few, nearest first, hidden classes,
// dense extras and asked-for exclusions left out.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createControls } from './controls.js';
function controlsWith(satellites, { classes, dense = [] } = {}) {
const state = {
_points: new Map(),
_catalog: new Map(),
_denseIds: dense,
_params: classes ? { classes } : {},
};
for (const sat of satellites) {
state._points.set(sat.id, {
position: Cesium.Cartesian3.fromDegrees(sat.lon, sat.lat, sat.alt),
});
state._catalog.set(sat.id, {
name: `${sat.name} `,
satelliteClass: sat.cls,
group: sat.group,
});
}
return createControls({
state,
services: { layerState: { isExplicitLayerStateOrigin: () => false } },
parts: {},
}).methods;
}
const SATELLITES = [
{ id: 1, name: 'NEAR', lat: 30.5, lon: -97, alt: 500_000, cls: 'imaging' },
{ id: 2, name: 'MID', lat: 33, lon: -97, alt: 500_000, cls: 'stations' },
{ id: 3, name: 'FAR', lat: -40, lon: 80, alt: 500_000, cls: 'imaging' },
{ id: 4, name: 'DEB', lat: 30.1, lon: -97, alt: 500_000, cls: 'debris' },
{ id: 5, name: 'TELECOM', lat: 30.2, lon: -97, alt: 500_000, cls: 'telecom' },
{ id: 6, name: 'SELF', lat: 30, lon: -97, alt: 420_000, cls: 'stations' },
{ id: 7, name: 'EXTRA', lat: 30.3, lon: -97, alt: 500_000, cls: 'imaging', group: 'dense' },
];
const CENTRE = Cesium.Cartesian3.fromDegrees(-97, 30, 420_000);
test('the nearest shown satellites come first, bounded, with their distance', () => {
const methods = controlsWith(SATELLITES, {
classes: { stations: true, imaging: true, debris: true, telecom: false },
dense: [7],
});
const nearest = methods.getNearestPositions(CENTRE, 3, {
excludeIds: [6],
excludeClasses: ['debris'],
});
assert.deepEqual(
nearest.map((entry) => entry.label),
['NEAR', 'MID', 'FAR'],
'no self, debris, hidden telecom or dense extra',
);
for (let i = 1; i < nearest.length; i += 1)
assert.ok(nearest[i].distanceM >= nearest[i - 1].distanceM);
const exact = Cesium.Cartesian3.distance(CENTRE, nearest[0].position);
assert.ok(Math.abs(nearest[0].distanceM - exact) < 1e-6);
assert.ok(Math.abs(nearest[0].latitude - 30.5) < 1e-6);
assert.equal(nearest[0].satelliteClass, 'imaging');
assert.equal(methods.getNearestPositions(CENTRE, 1, { excludeIds: [6] })[0].label, 'DEB');
assert.deepEqual(methods.getNearestPositions(null, 3), []);
});