yashwrites's picture
Signs on the film, draw tool, My search off by default, example films and Radiopaedia links (part 2)
2e6a666 verified
Raw History Blame Contribute Delete
8.67 kB
// Runtime guards for GET /api/reference (round 3; volumetric fields round 4). The backend lands in parallel, so
// every field is optional here: whatever is missing renders as an empty state instead of breaking the drawer.
import { FOCAL_LABELS_BY_MODALITY, isModality } from '../api/labels';
import type { Modality } from '../types/contracts';
export type Pt = [number, number];
/** A CT / MR example: the voxels and the label mask (both gzipped), decoded in the browser; `slice` is the axial
* slice through the finding's longest diameter (`measure.slice`), the one the card shows first. */
export type ReferenceVolume = {
volume_url: string;
mask_url: string;
shape: [number, number, number];
spacing: [number, number, number];
window: { wc: number; ww: number };
slice: number;
/** Mask values of the finding (organs are other values); empty = outline every non-zero voxel. */
label_values: number[];
};
export type ReferenceFilm = { case_id: string; image_url: string; width: number; height: number; modality: Modality; volume: ReferenceVolume | null; provenance: unknown };
export type ReferenceExample = ReferenceFilm & {
finding: { finding_id: string; polygon: Pt[] | null; bbox: [number, number, number, number] | null; relative_location: string; side: string | null; slice_range: [number, number] | null } | null;
};
export type ReferenceLabel = {
label: string;
display: string;
kind: 'focal' | 'pattern';
/** The scan type this finding is read on (from the payload, else the taxonomy's per-modality lists). */
modality: Modality;
one_liner: string;
key_signs: string[];
mimics: string[];
commonly_confused_with: string[];
search_tip: string;
radiopaedia_url: string | null;
review_status: string;
examples: ReferenceExample[];
/** Round 5: ids of the sign schematics (GET /api/signs) that belong to this finding type. */
signs: string[];
};
export type Reference = { labels: ReferenceLabel[]; normal_examples: ReferenceFilm[] };
type Obj = Record<string, unknown>;
const isObj = (v: unknown): v is Obj => typeof v === 'object' && v !== null && !Array.isArray(v);
const str = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
const strs = (v: unknown): string[] => (Array.isArray(v) ? v.map(str).filter(Boolean) : []);
const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
const isPt = (v: unknown): v is Pt => Array.isArray(v) && v.length >= 2 && typeof v[0] === 'number' && typeof v[1] === 'number';
const triple = (v: unknown): [number, number, number] | null =>
Array.isArray(v) && v.length >= 3 && v.slice(0, 3).every((n) => typeof n === 'number' && Number.isFinite(n) && n > 0) ? [v[0], v[1], v[2]] : null;
/** The volume block of a CT / MR example, when it is complete enough to render a slice. */
export function volumeOf(v: Obj, measureSlice: number | null): ReferenceVolume | null {
const volume_url = str(v.volume_url);
const mask_url = str(v.mask_url);
const shape = triple(v.shape);
if (!volume_url || !mask_url || !shape) return null;
const spacing = triple(v.spacing) ?? [1, 1, 1];
const w = isObj(v.window) ? v.window : {};
const window = { wc: typeof w.wc === 'number' ? w.wc : 0, ww: typeof w.ww === 'number' && w.ww > 0 ? w.ww : 1000 };
const mid = Math.floor(shape[0] / 2);
const slice = measureSlice != null ? Math.min(shape[0] - 1, Math.max(0, Math.round(measureSlice))) : mid;
const label_values = Array.isArray(v.label_values) ? v.label_values.filter((x): x is number => typeof x === 'number') : [];
return { volume_url, mask_url, shape, spacing, window, slice, label_values };
}
function film(v: unknown): ReferenceFilm | null {
if (!isObj(v)) return null;
const image_url = str(v.image_url);
const width = num(v.width);
const height = num(v.height);
const g = isObj(v.finding) ? v.finding : null;
const m = isObj(g?.measure) ? g.measure : null;
const volume = volumeOf(v, typeof m?.slice === 'number' ? m.slice : null);
const modality: Modality = isModality(v.modality) ? v.modality : volume ? 'ct' : 'cxr';
// An X-ray needs its image; a volume example can stand without a preview image.
if ((!image_url || width <= 0 || height <= 0) && !volume) return null;
return { case_id: str(v.case_id), image_url, width, height, modality, volume, provenance: isObj(v.provenance) ? v.provenance : null };
}
function example(v: unknown): ReferenceExample | null {
const f = film(v);
if (!f || !isObj(v)) return null;
const g = isObj(v.finding) ? v.finding : null;
if (!g) return { ...f, finding: null };
const polygon = Array.isArray(g.polygon) ? g.polygon.filter(isPt).map((p) => [p[0], p[1]] as Pt) : [];
const b = Array.isArray(g.bbox) && g.bbox.length >= 4 && g.bbox.every((n) => typeof n === 'number') ? (g.bbox.slice(0, 4) as [number, number, number, number]) : null;
const sr = Array.isArray(g.slice_range) && g.slice_range.length >= 2 && typeof g.slice_range[0] === 'number' && typeof g.slice_range[1] === 'number' ? ([g.slice_range[0], g.slice_range[1]] as [number, number]) : null;
if (f.volume && !f.volume.label_values.length && Array.isArray(g.label_values)) f.volume.label_values = g.label_values.filter((x): x is number => typeof x === 'number');
return {
...f,
finding: {
finding_id: str(g.finding_id),
polygon: polygon.length >= 3 ? polygon : null,
bbox: b,
relative_location: str(g.relative_location),
side: str(g.side) || null,
slice_range: sr,
},
};
}
/** Which scan type a finding is read on, from the taxonomy's per-modality lists (X-ray when unknown). */
export function modalityOfLabel(id: string): Modality {
for (const m of ['ct', 'mr'] as const) if (FOCAL_LABELS_BY_MODALITY[m].some((l) => l.id === id)) return m;
return 'cxr';
}
/** Only a plain https link is rendered as "Read more on Radiopaedia" (link-only; nothing is fetched from it). */
export function safeExternalUrl(v: unknown): string | null {
const u = str(v);
if (!u) return null;
try {
const p = new URL(u);
return p.protocol === 'https:' ? p.toString() : null;
} catch {
return null;
}
}
function label(v: unknown): ReferenceLabel | null {
if (!isObj(v)) return null;
const id = str(v.label ?? v.id);
if (!id) return null;
return {
label: id,
display: str(v.display ?? v.display_name) || id.replace(/_/g, ' '),
kind: v.kind === 'pattern' ? 'pattern' : 'focal',
modality: isModality(v.modality) ? v.modality : modalityOfLabel(id),
one_liner: str(v.one_liner),
key_signs: strs(v.key_signs),
mimics: strs(v.mimics),
commonly_confused_with: strs(v.commonly_confused_with),
search_tip: str(v.search_tip),
radiopaedia_url: safeExternalUrl(v.radiopaedia_url),
review_status: str(v.review_status ?? (isObj(v.review) ? v.review.status : '')),
examples: Array.isArray(v.examples) ? v.examples.map(example).filter((e): e is ReferenceExample => !!e) : [],
signs: strs(v.signs),
};
}
/** GET /api/reference/{label}: one entry of `labels[]` (round 5: with its `signs`). */
export const guardReferenceLabel = (v: unknown): ReferenceLabel | null => label(v);
/** The example films a debrief row shows (round 5): ones with an outline to draw (polygon, bbox or a volume),
* distinct cases, never the case being read, up to `n`. Falls back to any example when none has an outline. */
export function pickExamples(examples: ReferenceExample[], n = 2, excludeCaseId: string | null = null): ReferenceExample[] {
const out: ReferenceExample[] = [];
const seen = new Set<string>();
const take = (list: ReferenceExample[]) => {
for (const e of list) {
if (out.length >= n) return;
const key = e.case_id || e.image_url || e.volume?.volume_url || '';
if (excludeCaseId && e.case_id === excludeCaseId) continue;
if (seen.has(key)) continue;
seen.add(key);
out.push(e);
}
};
take(examples.filter((e) => e.volume || e.finding?.polygon || e.finding?.bbox));
take(examples);
return out;
}
export function guardReference(v: unknown): Reference {
if (!isObj(v)) return { labels: [], normal_examples: [] };
return {
labels: Array.isArray(v.labels) ? v.labels.map(label).filter((l): l is ReferenceLabel => !!l) : [],
normal_examples: Array.isArray(v.normal_examples) ? v.normal_examples.map(film).filter((f): f is ReferenceFilm => !!f) : [],
};
}
/** UI wording: the lens is called the magnifier everywhere (teaching cards written earlier still say "loupe"). */
export function uiTerms(text: string): string {
return text.replace(/\bloupe\b/g, 'magnifier').replace(/\bLoupe\b/g, 'Magnifier');
}