// Saamid overhead map
// Renders mic array, drone position, fading trail, range rings, grid, bearing ticks
// Map style: 'hybrid' (default), 'radar' (rings only), 'grid' (grid only), 'sat' (terrain blob)
// React hooks accessed via React.* to avoid global-scope name clashes across Babel files
function SaamidMap({
mics,
drone, // {x, y, alt, prob, type} or null
trail, // array of {x, y, alpha}
mapStyle, // 'hybrid' | 'radar' | 'grid' | 'sat'
threatType, // 'clear' | 'detected' | 'decoy' | 'unknown'
T, // i18n
dir, // 'rtl' | 'ltr'
onMicMove,
showMicLabels,
showCompass,
highlightedMicId,
}) {
// World bounds: -80..+80 metres in x and y, mic array centred near (1,1)
const W = 100; // half-width metres
const cx = 1, cy = 1; // map centre in metres (mic array centroid)
const VIEW = 800; // px viewBox
const scale = VIEW / (W * 2);
const wx = (mx) => (mx - cx + W) * scale;
const wy = (my) => VIEW - (my - cy + W) * scale; // y up
const ringDistances = [10, 25, 50, 75, 100]; // metres
// bearing & range from origin (mic centroid)
const bearingDeg = drone
? (Math.atan2(drone.x - cx, drone.y - cy) * 180 / Math.PI + 360) % 360
: null;
const range = drone
? Math.sqrt((drone.x - cx) ** 2 + (drone.y - cy) ** 2)
: null;
// colors via CSS vars
const cs = useThemeVars();
return (
{/* Compass overlay (top-end corner) */}
{showCompass !== false && (
{T.bearing}
{bearingDeg != null ? Math.round(bearingDeg) : "—"}°
{T.distance}
{range != null ? range.toFixed(1) : "—"} {T.range_m}
)}
);
}
function MicArrayInset({ mics, cs, dir, T }) {
// Tiny inset showing the mic array enlarged (since it's only ~2m, hard to see at world scale)
// Inset is bigger so 2m geometry is legible; pad symmetrically and use
// the largest of (extent, 2m) so a single-mic edit doesn't blow up scale.
const PAD = 0.6;
const xs = mics.map(m => m.x), ys = mics.map(m => m.y);
const cx = (Math.min(...xs) + Math.max(...xs)) / 2;
const cy = (Math.min(...ys) + Math.max(...ys)) / 2;
const ext = Math.max(
Math.max(...xs) - Math.min(...xs),
Math.max(...ys) - Math.min(...ys),
2.0,
);
const half = ext / 2 + PAD;
const minX = cx - half, maxX = cx + half;
const minY = cy - half, maxY = cy + half;
const w = maxX - minX, h = maxY - minY;
const SIZE = 180;
const s = SIZE / Math.max(w, h);
const tx = (m) => (m.x - minX) * s;
const ty = (m) => SIZE - (m.y - minY) * s;
// pin to bottom-end corner of the SVG viewBox (800)
const x = dir === "rtl" ? 16 : 800 - SIZE - 16;
const y = 800 - SIZE - 16;
// Quadrant-aware label offset so labels don't overlap the marker glyph
// or its neighbours: NE label sits NE of marker, etc.
const labelOffset = (m) => {
const dx = m.x - cx, dy = m.y - cy;
const ox = dx >= 0 ? 9 : -9;
const oy = dy >= 0 ? -9 : 13;
const anchor = dx >= 0 ? "start" : "end";
return { ox, oy, anchor };
};
// bounding 2m square outline (anchored at min corner of the actual array)
const aMinX = Math.min(...xs), aMinY = Math.min(...ys);
const aMaxX = Math.max(...xs), aMaxY = Math.max(...ys);
const boxX1 = (aMinX - minX) * s, boxX2 = (aMaxX - minX) * s;
const boxY1 = SIZE - (aMaxY - minY) * s, boxY2 = SIZE - (aMinY - minY) * s;
return (
{/* No title text — Arabic translation overflows the inset width.
The dashed bounding box, M1-Mn labels and the 1m scale bar make
this panel self-explanatory. */}
{/* bounding 2m square outline */}
{/* dimension labels on the bounding box */}
{(aMaxX - aMinX).toFixed(1)}m
{(aMaxY - aMinY).toFixed(1)}m
{mics.map((m) => {
const lo = labelOffset(m);
return (
M{m.id}
);
})}
{/* scale bar */}
1m
);
}
// helper to read CSS vars at render time
function useThemeVars() {
const [vars, setVars] = React.useState(() => readVars());
React.useEffect(() => {
const mo = new MutationObserver(() => setVars(readVars()));
mo.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
return () => mo.disconnect();
}, []);
return vars;
}
function readVars() {
const cs = getComputedStyle(document.documentElement);
const v = (name) => cs.getPropertyValue(name).trim() || "#000";
return {
mapBg: v("--map-bg"),
mapInk: v("--map-ink"),
bg: v("--bg"),
bgElev: v("--bg-elev"),
text: v("--text"),
textMuted: v("--text-muted"),
textFaint: v("--text-faint"),
accent: v("--accent"),
accent2: v("--accent-2"),
gridLine: v("--grid-line"),
gridLineStrong: v("--grid-line-strong"),
surfaceLine: v("--surface-line"),
surfaceLineStrong: v("--surface-line-strong"),
charcoal: v("--charcoal-900"),
};
}
Object.assign(window, { SaamidMap, useThemeVars });