fix(hud): carry rounded DMS seconds into the minutes (#1339)

Carry rounded HUD coordinate seconds into minutes and degrees.

Contribution by @rudycelekli.
This commit is contained in:
Rudy Mizrahi Celekli authored and GitHub committed 2026-10-25 17:59:09 -07:00
1 parent 2a17491c8b
commit a15fdd3134
5 files changed
+123 -6

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
- Carry HUD LAT/LON seconds that round up to 60.00" into the minutes and
degrees, so longitude -122.35 reads `122°21'00.00"W` instead of
`122°20'60.00"W` (rudycelekli, #1339).
+6
View File
@@ -4,6 +4,12 @@ Docker Compose mounts the checkout with the shared SELinux `:z` label so
containers can read it on enforcing SELinux hosts. The separate image-owned containers can read it on enforcing SELinux hosts. The separate image-owned
`node_modules` volume and loopback-only port publishing remain unchanged. `node_modules` volume and loopback-only port publishing remain unchanged.
The HUD's LAT/LON degrees-minutes-seconds readout rounds the coordinate once, to
hundredths of a second, and then splits it. A value within 0.005" of the next
whole minute carries into the minutes and degrees, so `-122.35` reads
`122°21'00.00"W` rather than `122°20'60.00"W`. The field widths and hemisphere
letters are unchanged.
Aircraft detection candidates reuse normalized display text until callsign or Aircraft detection candidates reuse normalized display text until callsign or
registration changes, including in-place enrichment. Each candidate resolves its registration changes, including in-place enrichment. Each candidate resolves its
fleet model once per collection. Positions, model ownership and tracked-aircraft fleet model once per collection. Positions, model ownership and tracked-aircraft
+2 -1
View File
@@ -1087,5 +1087,6 @@
"scripts/qa-status-badges.mjs", "scripts/qa-status-badges.mjs",
"src/dockerTrustedPeers.test.mjs", "src/dockerTrustedPeers.test.mjs",
"src/cameraGlobeRotation.test.mjs", "src/cameraGlobeRotation.test.mjs",
"src/voice/globeResetConfirmation.test.mjs" "src/voice/globeResetConfirmation.test.mjs",
"src/hudDms.test.mjs"
] ]
+6 -5
View File
@@ -574,11 +574,12 @@ export class IntelHUD {
* @returns {string} Formatted DMS string, e.g. `"38°53'23.10"N"`. * @returns {string} Formatted DMS string, e.g. `"38°53'23.10"N"`.
*/ */
_toDMS(decimal, type) { _toDMS(decimal, type) {
const abs = Math.abs(decimal); // Round once, in hundredths of a second, so a value that rounds up to
const deg = Math.floor(abs); // 60.00" carries into the minutes and degrees instead of printing 60.
const minFloat = (abs - deg) * 60; const hundredths = Math.round(Math.abs(decimal) * 360000);
const min = Math.floor(minFloat); const deg = Math.floor(hundredths / 360000);
const sec = ((minFloat - min) * 60).toFixed(2); const min = Math.floor(hundredths / 6000) % 60;
const sec = ((hundredths % 6000) / 100).toFixed(2);
let dir; let dir;
if (type === 'lat') dir = decimal >= 0 ? 'N' : 'S'; if (type === 'lat') dir = decimal >= 0 ? 'N' : 'S';
+106
View File
@@ -0,0 +1,106 @@
// The HUD's LAT/LON readout in degrees-minutes-seconds. Seconds are printed to
// two decimals, so a value within 0.005" of the next whole minute rounds up.
// That carry must reach the minutes (and degrees): `122°20'60.00"W` is not a
// coordinate. Ordinary decimal inputs land there through binary floating
// point: 122.35 - 122 is 0.34999999999999432, so -122.35 read 20'60.00".
//
// hud.js imports `mgrs`, a CommonJS package whose named exports Node's ESM
// loader cannot see, so this installs the same one-specifier stub as
// hudAltitudeDatum.test.mjs before importing it.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { registerHooks } from 'node:module';
const MGRS_STUB_URL = 'gev-test-stub:mgrs';
registerHooks({
resolve(specifier, context, next) {
if (specifier === 'mgrs') return { url: MGRS_STUB_URL, shortCircuit: true };
return next(specifier, context);
},
load(url, context, next) {
if (url === MGRS_STUB_URL) {
return {
format: 'module',
shortCircuit: true,
source:
'export function forward() { return "10SEG55776339"; }\nexport default { forward };\n',
};
}
return next(url, context);
},
});
const { IntelHUD } = await import('./hud.js');
const toDMS = (decimal, type) => IntelHUD.prototype._toDMS(decimal, type);
test('a rounded-up second carries into the minutes', () => {
assert.equal(toDMS(-122.35, 'lon'), `122°21'00.00"W`);
assert.equal(toDMS(139.7, 'lon'), `139°42'00.00"E`);
assert.equal(toDMS(37.0166666, 'lat'), `37°01'00.00"N`);
});
test('a carry through 59 minutes reaches the degrees', () => {
assert.equal(toDMS(38.999999, 'lat'), `39°00'00.00"N`);
assert.equal(toDMS(-0.999999, 'lat'), `01°00'00.00"S`);
assert.equal(toDMS(179.999999, 'lon'), `180°00'00.00"E`);
});
test('ordinary values keep their existing format', () => {
assert.equal(toDMS(38.889722, 'lat'), `38°53'23.00"N`);
assert.equal(toDMS(-77.035278, 'lon'), `077°02'07.00"W`);
assert.equal(toDMS(51.5, 'lat'), `51°30'00.00"N`);
assert.equal(toDMS(-33.8568, 'lat'), `33°51'24.48"S`);
assert.equal(toDMS(0, 'lon'), `000°00'00.00"E`);
assert.equal(toDMS(90, 'lat'), `90°00'00.00"N`);
});
test('the telemetry tick paints the carried value into both readouts', () => {
const elements = new Map(
[
'hud-alt',
'hud-summary',
'hud-mgrs',
'hud-latlon',
'hud-bottom-line',
'hud-gsd',
'hud-coll',
'hud-ona',
'hud-mode',
].map((id) => [id, { textContent: '' }]),
);
const previousDocument = globalThis.document;
globalThis.document = {
getElementById: (id) => elements.get(id) ?? null,
querySelectorAll: () => [],
addEventListener() {},
removeEventListener() {},
};
const viewer = {
camera: {
pitch: -Math.PI / 2,
positionCartographic: {
latitude: (37.75 * Math.PI) / 180,
longitude: (-122.35 * Math.PI) / 180,
height: 1500,
},
computeViewRectangle: () => undefined,
moveStart: { addEventListener() {}, removeEventListener() {} },
moveEnd: { addEventListener() {}, removeEventListener() {} },
},
};
let hud;
try {
hud = new IntelHUD(viewer);
hud._updateCameraData();
const latlon = elements.get('hud-latlon').textContent;
const bottom = elements.get('hud-bottom-line').textContent;
assert.equal(latlon, `37°45'00.00"N 122°21'00.00"W`);
assert.match(bottom, /LON: 122°21'00\.00"W/);
assert.doesNotMatch(`${latlon} ${bottom}`, /60\.00"/);
} finally {
hud?.destroy();
if (previousDocument === undefined) delete globalThis.document;
else globalThis.document = previousDocument;
}
});