merge main 0c28dc7 into feat/voice-outlines

This commit is contained in:
manjunath committed 2026-11-02 11:39:21 +05:30
commit d9a460658d
17 files changed
+344 -18

No files matched your search

+21
View File
@@ -1321,6 +1321,27 @@ test('buildVesselCard: heading falls back to course; missing parts are omitted',
assert.deepEqual(bare.details, []);
});
test('vessel cards read a course that rounds to north as 0°, not 360°', () => {
const card = (course) => buildVesselCard(makeRecord({ heading: null, course })).details[0];
assert.equal(card(359.4), 'CONTAINER SHIP · 14.5KT · 359°');
assert.equal(card(359.7), 'CONTAINER SHIP · 14.5KT · 0°');
assert.equal(
buildSelectedVesselCard(makeRecord({ heading: null, course: 359.7 })).details[0],
'CONTAINER SHIP · 14.5KT · 0°',
);
});
test('the vessel HUD reads a course that rounds to north as HDG 0DEG', () => {
const harness = installWireHarness(undefined, { selectedRecord: null });
try {
Object.assign(harness.record, { heading: null, course: 359.7 });
assert.equal(aisLiveVesselsLayer.selectById(harness.record.mmsi), true);
assert.match(harness.hud.readout.text, /HDG: 0DEG/);
} finally {
harness.cleanup();
}
});
test('buildVesselCard: unnamed vessels title as MMSI; long names truncate', () => {
const unnamed = buildVesselCard(makeRecord({ name: 'VESSEL' }));
assert.equal(unnamed.title, 'MMSI 353136000');
+18
View File
@@ -2,6 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createAlprOverlay } from './overlay.js';
import { normalizeAlprNode } from './records.js';
import {
directionWedgePositions,
alprLabelDetails,
@@ -218,6 +219,23 @@ test('adapter labels never claim a custom source is public OSM data', () => {
);
});
test('a mapped bearing that rounds up to a whole turn reads 0°, not 360°', () => {
const direction = (tag) =>
alprLabelDetails(
normalizeAlprNode({
type: 'node',
id: 1,
lat: 30,
lon: -97,
tags: { 'surveillance:type': 'ALPR', 'camera:direction': tag },
}),
{ label: 'Custom directory' },
).find((line) => line.startsWith('DIRECTION'));
assert.equal(direction('359.6'), 'DIRECTION 0°');
assert.equal(direction('359.4'), 'DIRECTION 359°');
assert.equal(direction('0.4'), 'DIRECTION 0°');
});
test('overlay glyphs and native badges shrink alike with distance', () => {
assert.equal(markerScale(100), 1.15);
assert.equal(markerScale(40_000), 0.45);
+2 -1
View File
@@ -49,8 +49,9 @@ export function alprLabelDetails(record, source) {
const details = [
sourceName === 'OpenStreetMap' ? 'OSM MAPPED' : `Source: ${sourceName}`,
];
// Wrap after rounding: 359.6° rounds to 360°, which is north.
if (Number.isFinite(record.directionDeg))
details.push(`DIRECTION ${Math.round(record.directionDeg)}°`);
details.push(`DIRECTION ${Math.round(record.directionDeg) % 360}°`);
const equipment = [
...new Set(
[record.manufacturer, record.operator, record.cameraType]
+2 -1
View File
@@ -44,7 +44,8 @@ export function isHeadingEstimated(camera) {
* @returns {string}
*/
export function headingHudToken(camera) {
const hdg = Math.round(Number(camera?.headingDeg) || 0);
// 359.5° and above rounds to north; modulo keeps it from printing 360°.
const hdg = Math.round(Number(camera?.headingDeg) || 0) % 360;
return isHeadingEstimated(camera)
? t('HDG {value}° (ESTIMATED)', { value: hdg })
: t('HDG {value}°', { value: hdg });
@@ -44,6 +44,15 @@ test('HUD token tags a synthetic bearing and rounds the degrees', () => {
);
});
test('HUD token reads a bearing that rounds to north as 0°, not 360°', () => {
assert.equal(headingHudToken({ headingDeg: 359.4 }), 'HDG 359°');
assert.equal(headingHudToken({ headingDeg: 359.6 }), 'HDG 0°');
assert.equal(
headingHudToken({ headingDeg: 359.5, headingConfidence: 'low' }),
'HDG 0° (ESTIMATED)',
);
});
test('HUD token stays untagged for surveyed and calibrated bearings', () => {
assert.equal(
headingHudToken({ headingDeg: 67.5, headingConfidence: 'high' }),
+6 -3
View File
@@ -55,7 +55,10 @@ export function createCards({
if (record.speed !== null && record.speed !== undefined)
parts.push(formatSpeed(record.speed));
const direction = record.heading ?? record.course;
if (Number.isFinite(direction)) parts.push(`${Math.round(direction)}°`);
// A course of 359.5° and above rounds to north; modulo keeps it from
// printing 360°. The selected card and the HUD wrap the same way.
if (Number.isFinite(direction))
parts.push(`${Math.round(direction) % 360}°`);
return {
id: vesselOverlayEntryId(record),
actionable: Boolean(record?.mmsi),
@@ -86,7 +89,7 @@ export function createCards({
[
vesselTypeShort(record) || t('VESSEL'),
formatSpeed(record.speed),
Number.isFinite(direction) ? `${Math.round(direction)}°` : '--°',
Number.isFinite(direction) ? `${Math.round(direction) % 360}°` : '--°',
].join(' · '),
];
const destination = String(record.destination || '').trim();
@@ -176,7 +179,7 @@ export function createCards({
function formatHeading(heading) {
return t('{heading}DEG', {
heading: Number.isFinite(heading) ? Math.round(heading) : '--',
heading: Number.isFinite(heading) ? Math.round(heading) % 360 : '--',
});
}
+111
View File
@@ -0,0 +1,111 @@
import assert from 'node:assert/strict';
import {
chmodSync,
mkdirSync,
mkdtempSync,
readdirSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
cachedVueModule,
compileVueModule,
ownCacheDirectory,
vueCacheDirectory,
vueCacheKey,
} from '../../build/vueLoader.js';
const SOURCE =
'<script setup>\nconst label = "Hi";\n</script>\n<template><b>{{ label }}</b></template>\n';
const FILE = '/virtual/Greeting.vue';
test('a component compiles once and later processes read the cached module', (t) => {
const directory = mkdtempSync(path.join(tmpdir(), 'gev-vue-cache-test-'));
t.after(() => rmSync(directory, { recursive: true, force: true }));
const compiled = cachedVueModule(SOURCE, FILE, directory);
assert.equal(compiled, compileVueModule(SOURCE, FILE));
const entries = readdirSync(directory);
assert.deepEqual(entries, [`${vueCacheKey(SOURCE, FILE)}.mjs`]);
// A cached entry is returned as stored, without compiling again.
writeFileSync(path.join(directory, entries[0]), 'export default "cached";');
assert.equal(
cachedVueModule(SOURCE, FILE, directory),
'export default "cached";',
);
});
test('an edited component or another path is a new cache entry', () => {
const key = vueCacheKey(SOURCE, FILE);
assert.notEqual(vueCacheKey(`${SOURCE}\n`, FILE), key);
assert.notEqual(vueCacheKey(SOURCE, '/virtual/Other.vue'), key);
assert.equal(vueCacheKey(SOURCE, FILE), key);
});
test('GEV_VUE_CACHE=0 compiles every time; otherwise the cache sits in a per-user directory', () => {
assert.equal(vueCacheDirectory({ GEV_VUE_CACHE: '0' }), null);
if (process.platform !== 'win32') {
const directory = vueCacheDirectory({});
assert.equal(path.dirname(directory), tmpdir());
assert.match(path.basename(directory), /^gev-vue-sfc-/);
}
// Windows temp directories may be shared, so the cache lives in the
// user's profile, or not at all.
assert.equal(
vueCacheDirectory(
{ LOCALAPPDATA: 'C:\\Users\\a\\AppData\\Local' },
'win32',
),
path.join('C:\\Users\\a\\AppData\\Local', 'gev-vue-sfc'),
);
assert.equal(vueCacheDirectory({}, 'win32'), null);
assert.equal(
cachedVueModule(SOURCE, FILE, null),
compileVueModule(SOURCE, FILE),
);
});
test('a cache directory that cannot be written still returns the compiled module', (t) => {
const directory = mkdtempSync(path.join(tmpdir(), 'gev-vue-cache-test-'));
t.after(() => rmSync(directory, { recursive: true, force: true }));
// A file where the cache directory should be makes every write fail.
const blocked = path.join(directory, 'blocked');
writeFileSync(blocked, '');
assert.equal(
cachedVueModule(SOURCE, FILE, blocked),
compileVueModule(SOURCE, FILE),
);
});
test(
'a cache directory others can write to, or a link to one, is never read',
{
skip: process.platform === 'win32',
},
(t) => {
const root = mkdtempSync(path.join(tmpdir(), 'gev-vue-cache-test-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const shared = path.join(root, 'shared');
mkdirSync(shared);
chmodSync(shared, 0o777);
// A module planted where the cache would look must not run.
writeFileSync(
path.join(shared, `${vueCacheKey(SOURCE, FILE)}.mjs`),
'export default "planted";',
);
assert.equal(ownCacheDirectory(shared), false);
assert.equal(
cachedVueModule(SOURCE, FILE, shared),
compileVueModule(SOURCE, FILE),
);
const own = path.join(root, 'own');
mkdirSync(own, { mode: 0o700 });
const link = path.join(root, 'link');
symlinkSync(own, link);
assert.equal(ownCacheDirectory(link), false);
assert.equal(ownCacheDirectory(own), true);
},
);
+23
View File
@@ -490,3 +490,26 @@ test('the scene summary types in, then holds the whole text', async (t) => {
await nextTick();
assert.equal(byId('cctv-summary').textContent, text);
});
test('the panel meta line reads a bearing that rounds to north as HDG 0°', () => {
const meta = (headingDeg) => {
const camera = {
id: 'a',
city: 'Austin',
headingDeg,
fovDeg: 74,
rangeM: 700,
};
return presentCctvPanel(
{
enabled: true,
activeCameraId: camera.id,
activeCamera: camera,
cameras: [camera],
},
true,
).meta;
};
assert.match(meta(359.4), /· HDG 359° ·/);
assert.match(meta(359.6), /· HDG 0° ·/);
});
+1 -1
View File
@@ -107,7 +107,7 @@ function metaText(state, enabled, activeCamera, catalogCount) {
'{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}',
{
city: activeCamera.city,
heading: `${Math.round(activeCamera.headingDeg)}°`,
heading: `${Math.round(activeCamera.headingDeg) % 360}°`,
fov: `${Math.round(activeCamera.fovDeg)}°`,
range: Math.round(activeCamera.rangeM),
projection: projLabel,