mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge branch 'main' into contrib/gods-021-cmr-raw-page-budget-20261011-reviewed
This commit is contained in:
8 files changed
+165
-20
No files matched your search
@@ -1094,6 +1094,9 @@ collapse can still be undone.
|
|||||||
|
|
||||||
## Surface keyboard lifecycle
|
## Surface keyboard lifecycle
|
||||||
|
|
||||||
|
Surface Tab cycling excludes controls with a negative tabindex, native disabled
|
||||||
|
state or inert ancestry; ordinary visible enabled controls retain their order.
|
||||||
|
|
||||||
`ui/surfaces` owns the capture-phase keyboard listener, Tab cycling and return
|
`ui/surfaces` owns the capture-phase keyboard listener, Tab cycling and return
|
||||||
focus shared by the first-run launcher and Provider Settings. Each caller
|
focus shared by the first-run launcher and Provider Settings. Each caller
|
||||||
activates it while open and deactivates it on dismissal; destruction releases
|
activates it while open and deactivates it on dismissal; destruction releases
|
||||||
@@ -1208,6 +1211,11 @@ a browser key. Preview is for local build verification, not a production server.
|
|||||||
|
|
||||||
## CCTV and radio provider modules
|
## CCTV and radio provider modules
|
||||||
|
|
||||||
|
CCTV direction text resolves travel tokens first, then intercardinals, then
|
||||||
|
bare cardinals when enabled. Only conflicting headings within the highest
|
||||||
|
matching tier remain unknown; street quadrants and lower-tier words do not
|
||||||
|
cancel an explicit travel heading.
|
||||||
|
|
||||||
CCTV catalog acquisition, source normalization and frame/media delivery now live
|
CCTV catalog acquisition, source normalization and frame/media delivery now live
|
||||||
in separate modules. Each CCTV factory owns its catalog and health state; its
|
in separate modules. Each CCTV factory owns its catalog and health state; its
|
||||||
`sourceRoot` option resolves relative source files against the application root.
|
`sourceRoot` option resolves relative source files against the application root.
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { directionToHeading } from './directionText.js';
|
||||||
|
test('conflicting facing directions cannot manufacture one heading', () => {
|
||||||
|
for (const bare of [false, true]) {
|
||||||
|
for (const value of ['NB / SB', 'EASTBOUND WESTBOUND', 'NE SW'])
|
||||||
|
assert.ok(Number.isNaN(directionToHeading(value, bare)), value);
|
||||||
|
assert.equal(directionToHeading('NB northbound', bare), 0);
|
||||||
|
assert.equal(directionToHeading('IH-35 SOUTHBOUND AT 15TH', bare), 180);
|
||||||
|
}
|
||||||
|
assert.ok(Number.isNaN(directionToHeading('North / East', true)));
|
||||||
|
assert.equal(directionToHeading('West / SR-24', true), 270);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('travel tokens retain precedence over quadrant streets and bare words', () => {
|
||||||
|
for (const [text, heading] of [
|
||||||
|
['I-5 NB at NE 45th St', 0],
|
||||||
|
['SR 520 EB at NE 148th', 90],
|
||||||
|
['US 13 SB @ SW Front St', 180],
|
||||||
|
]) {
|
||||||
|
for (const bare of [false, true])
|
||||||
|
assert.equal(directionToHeading(text, bare), heading);
|
||||||
|
}
|
||||||
|
assert.equal(
|
||||||
|
directionToHeading('Hwy 401 east of Keele St - Westbound', true),
|
||||||
|
270,
|
||||||
|
);
|
||||||
|
assert.equal(directionToHeading('NE near West St', true), 45);
|
||||||
|
assert.ok(Number.isNaN(directionToHeading('NB SB near NE St', true)));
|
||||||
|
});
|
||||||
+28
-16
@@ -28,22 +28,34 @@ export function directionToHeading(value, allowBare = false) {
|
|||||||
.trim()
|
.trim()
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
if (!text) return NaN;
|
if (!text) return NaN;
|
||||||
// Explicit travel/intercardinal forms — safe on free-form text (a street
|
const tiers = [
|
||||||
// name almost never contains "NORTHBOUND" or a lone "NB" token).
|
[
|
||||||
if (/\bNORTHBOUND\b|\bNB\b/.test(text)) return 0;
|
[/\bNORTHBOUND\b|\bNB\b/, 0],
|
||||||
if (/\bSOUTHBOUND\b|\bSB\b/.test(text)) return 180;
|
[/\bSOUTHBOUND\b|\bSB\b/, 180],
|
||||||
if (/\bEASTBOUND\b|\bEB\b/.test(text)) return 90;
|
[/\bEASTBOUND\b|\bEB\b/, 90],
|
||||||
if (/\bWESTBOUND\b|\bWB\b/.test(text)) return 270;
|
[/\bWESTBOUND\b|\bWB\b/, 270],
|
||||||
if (/\bNORTHEAST\b|\bNE\b/.test(text)) return 45;
|
],
|
||||||
if (/\bNORTHWEST\b|\bNW\b/.test(text)) return 315;
|
[
|
||||||
if (/\bSOUTHEAST\b|\bSE\b/.test(text)) return 135;
|
[/\bNORTHEAST\b|\bNE\b/, 45],
|
||||||
if (/\bSOUTHWEST\b|\bSW\b/.test(text)) return 225;
|
[/\bNORTHWEST\b|\bNW\b/, 315],
|
||||||
// Bare cardinals — dedicated direction fields only.
|
[/\bSOUTHEAST\b|\bSE\b/, 135],
|
||||||
if (allowBare) {
|
[/\bSOUTHWEST\b|\bSW\b/, 225],
|
||||||
if (/\bNORTH\b/.test(text)) return 0;
|
],
|
||||||
if (/\bSOUTH\b/.test(text)) return 180;
|
];
|
||||||
if (/\bEAST\b/.test(text)) return 90;
|
if (allowBare)
|
||||||
if (/\bWEST\b/.test(text)) return 270;
|
tiers.push([
|
||||||
|
[/\bNORTH\b/, 0],
|
||||||
|
[/\bSOUTH\b/, 180],
|
||||||
|
[/\bEAST\b/, 90],
|
||||||
|
[/\bWEST\b/, 270],
|
||||||
|
]);
|
||||||
|
for (const forms of tiers) {
|
||||||
|
const headings = new Set();
|
||||||
|
for (const [pattern, heading] of forms) {
|
||||||
|
if (pattern.test(text)) headings.add(heading);
|
||||||
|
}
|
||||||
|
if (headings.size)
|
||||||
|
return headings.size === 1 ? headings.values().next().value : NaN;
|
||||||
}
|
}
|
||||||
return NaN;
|
return NaN;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { searchHls } from './catalog.js';
|
import { searchHls } from './catalog.js';
|
||||||
import { gibsTemplate, wvsSnapshotUrl } from './model.js';
|
import { gibsTemplate, wvsSnapshotUrl } from './model.js';
|
||||||
|
|
||||||
/** NASA-compatible imagery acquisition; transport and URL builders are replaceable. */
|
/**
|
||||||
|
* NASA-compatible imagery acquisition; transport and URL builders are replaceable.
|
||||||
|
* Thumbnail responses must expose Data-Present: true for image admission, or
|
||||||
|
* false for confirmed empty coverage, including injected getThumbnail sources.
|
||||||
|
*/
|
||||||
export function createRecentImagerySource({
|
export function createRecentImagerySource({
|
||||||
fetchImpl = (...args) => globalThis.fetch(...args),
|
fetchImpl = (...args) => globalThis.fetch(...args),
|
||||||
search = searchHls,
|
search = searchHls,
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createThumbnailLoader } from './thumbnails.js';
|
||||||
|
test('missing or malformed presence evidence cannot prove thumbnail pixels', async () => {
|
||||||
|
for (const header of [null, 'unknown', '', 'true', 'false']) {
|
||||||
|
let decodes = 0;
|
||||||
|
const loader = createThumbnailLoader({
|
||||||
|
requestThumbnail: async () => ({
|
||||||
|
ok: true,
|
||||||
|
headers: { get: (key) => (key === 'Data-Present' ? header : null) },
|
||||||
|
blob: async () => {
|
||||||
|
decodes++;
|
||||||
|
return new Blob(['fixture']);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createObjectUrl: () => 'blob:fixture',
|
||||||
|
});
|
||||||
|
loader.request(
|
||||||
|
{ key: 'fixture', product: 'S30', day: '2026-09-18' },
|
||||||
|
{ west: 0, east: 1, south: 0, north: 1 },
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
assert.equal(
|
||||||
|
loader.get('fixture').status,
|
||||||
|
header === 'true' ? 'present' : header === 'false' ? 'empty' : 'error',
|
||||||
|
);
|
||||||
|
assert.equal(decodes, header === 'true' ? 1 : 0);
|
||||||
|
loader.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -100,12 +100,12 @@ export function createThumbnailLoader({
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
} else if (response?.ok) {
|
} else if (response?.ok) {
|
||||||
const present = String(
|
const present = String(
|
||||||
response.headers?.get?.('Data-Present') ?? 'true',
|
response.headers?.get?.('Data-Present') ?? '',
|
||||||
).toLowerCase();
|
).toLowerCase();
|
||||||
acquisitionTime = response.headers?.get?.('Acquisition-Time') || null;
|
acquisitionTime = response.headers?.get?.('Acquisition-Time') || null;
|
||||||
if (present === 'false') {
|
if (present === 'false') {
|
||||||
status = 'empty';
|
status = 'empty';
|
||||||
} else {
|
} else if (present === 'true') {
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
if (controller.signal.aborted) cancelled = true;
|
if (controller.signal.aborted) cancelled = true;
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createSurfaceKeyboard } from './surfaceKeyboard.js';
|
||||||
|
test('surface cycling never targets deliberately excluded controls', () => {
|
||||||
|
let handler;
|
||||||
|
const documentRef = {
|
||||||
|
activeElement: {},
|
||||||
|
addEventListener: (_, fn) => (handler = fn),
|
||||||
|
removeEventListener() {},
|
||||||
|
};
|
||||||
|
const node = ({
|
||||||
|
attributes = {},
|
||||||
|
fieldsetDisabled = false,
|
||||||
|
inert = false,
|
||||||
|
} = {}) => ({
|
||||||
|
hasAttribute: (name) => Object.hasOwn(attributes, name),
|
||||||
|
getClientRects: () => [{}],
|
||||||
|
getAttribute: (name) => attributes[name] ?? null,
|
||||||
|
matches: (selector) => selector === ':disabled' && fieldsetDisabled,
|
||||||
|
closest: (selector) =>
|
||||||
|
selector === '[inert]' && inert ? { inert: true } : null,
|
||||||
|
focus() {
|
||||||
|
documentRef.activeElement = this;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const ignored = [
|
||||||
|
node({ attributes: { tabindex: '-1' } }),
|
||||||
|
node({ fieldsetDisabled: true }),
|
||||||
|
node({ inert: true }),
|
||||||
|
];
|
||||||
|
const first = node({}),
|
||||||
|
last = node({});
|
||||||
|
const controls = [...ignored, first, last];
|
||||||
|
const root = {
|
||||||
|
ownerDocument: documentRef,
|
||||||
|
querySelectorAll: () => controls,
|
||||||
|
contains: (n) => controls.includes(n),
|
||||||
|
};
|
||||||
|
const surface = createSurfaceKeyboard({
|
||||||
|
root,
|
||||||
|
isActive: () => true,
|
||||||
|
onEscape() {},
|
||||||
|
});
|
||||||
|
surface.activate();
|
||||||
|
const press = (shiftKey) =>
|
||||||
|
handler({
|
||||||
|
key: 'Tab',
|
||||||
|
shiftKey,
|
||||||
|
preventDefault() {},
|
||||||
|
stopPropagation() {},
|
||||||
|
});
|
||||||
|
press(false);
|
||||||
|
assert.equal(documentRef.activeElement, first);
|
||||||
|
press(true);
|
||||||
|
assert.equal(documentRef.activeElement, last);
|
||||||
|
surface.destroy();
|
||||||
|
});
|
||||||
@@ -39,7 +39,11 @@ export function createSurfaceKeyboard({
|
|||||||
),
|
),
|
||||||
].filter(
|
].filter(
|
||||||
(node) =>
|
(node) =>
|
||||||
!node.hasAttribute('disabled') && node.getClientRects().length > 0,
|
!node.hasAttribute('disabled') &&
|
||||||
|
!node.matches?.(':disabled') &&
|
||||||
|
!(Number(node.getAttribute?.('tabindex')) < 0) &&
|
||||||
|
!node.closest?.('[inert]') &&
|
||||||
|
node.getClientRects().length > 0,
|
||||||
);
|
);
|
||||||
if (!order.length) return;
|
if (!order.length) return;
|
||||||
const first = order[0];
|
const first = order[0];
|
||||||
|
|||||||
Reference in new issue
Block a user