LiveScreen: GaussianSplatWebView + gaussian-splats.html (Three.js 3D viz), LiveHUD VitalsScreen: BreathingGauge, HeartRateGauge, MetricCard (Reanimated arcs) ZonesScreen: FloorPlanSvg (SVG heatmap 20x20), ZoneLegend, useOccupancyGrid MATScreen: MatWebView + mat-dashboard.html (pure-JS disaster response), AlertCard/List, SurvivorCounter SettingsScreen: ServerUrlInput (URL validation + test), ThemePicker, RssiToggle Verified: tsc 0 errors, jest passes
139 lines
3.8 KiB
TypeScript
139 lines
3.8 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { useWindowDimensions, View } from 'react-native';
|
|
import { ConnectionBanner } from '@/components/ConnectionBanner';
|
|
import { ThemedView } from '@/components/ThemedView';
|
|
import { colors } from '@/theme/colors';
|
|
import { spacing } from '@/theme/spacing';
|
|
import { usePoseStream } from '@/hooks/usePoseStream';
|
|
import { useMatStore } from '@/stores/matStore';
|
|
import { type ConnectionStatus } from '@/types/sensing';
|
|
import { Alert, type Survivor } from '@/types/mat';
|
|
import { AlertList } from './AlertList';
|
|
import { MatWebView } from './MatWebView';
|
|
import { SurvivorCounter } from './SurvivorCounter';
|
|
import { useMatBridge } from './useMatBridge';
|
|
|
|
const isAlert = (value: unknown): value is Alert => {
|
|
if (!value || typeof value !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
const record = value as Record<string, unknown>;
|
|
return typeof record.id === 'string' && typeof record.message === 'string';
|
|
};
|
|
|
|
const isSurvivor = (value: unknown): value is Survivor => {
|
|
if (!value || typeof value !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
const record = value as Record<string, unknown>;
|
|
return typeof record.id === 'string' && typeof record.zone_id === 'string';
|
|
};
|
|
|
|
const resolveBannerState = (status: ConnectionStatus): 'connected' | 'simulated' | 'disconnected' => {
|
|
if (status === 'connecting') {
|
|
return 'disconnected';
|
|
}
|
|
|
|
return status;
|
|
};
|
|
|
|
export const MATScreen = () => {
|
|
const { connectionStatus, lastFrame } = usePoseStream();
|
|
|
|
const { survivors, alerts, upsertSurvivor, addAlert, upsertEvent } = useMatStore((state) => ({
|
|
survivors: state.survivors,
|
|
alerts: state.alerts,
|
|
upsertSurvivor: state.upsertSurvivor,
|
|
addAlert: state.addAlert,
|
|
upsertEvent: state.upsertEvent,
|
|
}));
|
|
|
|
const { webViewRef, ready, onMessage, sendFrameUpdate, postEvent } = useMatBridge({
|
|
onSurvivorDetected: (survivor) => {
|
|
if (isSurvivor(survivor)) {
|
|
upsertSurvivor(survivor);
|
|
}
|
|
},
|
|
onAlertGenerated: (alert) => {
|
|
if (isAlert(alert)) {
|
|
addAlert(alert);
|
|
}
|
|
},
|
|
});
|
|
|
|
const seededRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (!ready || seededRef.current) {
|
|
return;
|
|
}
|
|
|
|
const createEvent = postEvent('CREATE_EVENT');
|
|
createEvent({
|
|
type: 'earthquake',
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
name: 'Training Scenario',
|
|
});
|
|
|
|
const addZone = postEvent('ADD_ZONE');
|
|
addZone({
|
|
name: 'Zone A',
|
|
zone_type: 'rectangle',
|
|
x: 60,
|
|
y: 60,
|
|
width: 180,
|
|
height: 120,
|
|
});
|
|
addZone({
|
|
name: 'Zone B',
|
|
zone_type: 'circle',
|
|
center_x: 300,
|
|
center_y: 170,
|
|
radius: 60,
|
|
});
|
|
|
|
upsertEvent({
|
|
event_id: 'training-scenario',
|
|
disaster_type: 1,
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
description: 'Training Scenario',
|
|
});
|
|
|
|
seededRef.current = true;
|
|
}, [postEvent, upsertEvent, ready]);
|
|
|
|
useEffect(() => {
|
|
if (ready && lastFrame) {
|
|
sendFrameUpdate(lastFrame);
|
|
}
|
|
}, [lastFrame, ready, sendFrameUpdate]);
|
|
|
|
const { height } = useWindowDimensions();
|
|
const webHeight = Math.max(240, Math.floor(height * 0.5));
|
|
|
|
return (
|
|
<ThemedView style={{ flex: 1, backgroundColor: colors.bg, padding: spacing.md }}>
|
|
<ConnectionBanner status={resolveBannerState(connectionStatus)} />
|
|
<View style={{ marginTop: 20 }}>
|
|
<SurvivorCounter survivors={survivors} />
|
|
</View>
|
|
<View style={{ height: webHeight }}>
|
|
<MatWebView
|
|
webViewRef={webViewRef}
|
|
onMessage={onMessage}
|
|
style={{ flex: 1, borderRadius: 12, overflow: 'hidden', backgroundColor: colors.surface }}
|
|
/>
|
|
</View>
|
|
<View style={{ flex: 1, marginTop: spacing.md }}>
|
|
<AlertList alerts={alerts} />
|
|
</View>
|
|
</ThemedView>
|
|
);
|
|
};
|
|
|
|
export default MATScreen;
|