Implement 6 optional enhancements for the wifi-Mat module: 1. Hardware Integration (csi_receiver.rs + hardware_adapter.rs) - ESP32 CSI support via serial/UDP - Intel 5300 BFEE file parsing - Atheros CSI Tool integration - Live UDP packet streaming - PCAP replay capability 2. CLI Commands (wifi-densepose-cli/src/mat.rs) - `wifi-mat scan` - Run disaster detection scan - `wifi-mat status` - Check event status - `wifi-mat zones` - Manage scan zones - `wifi-mat survivors` - List detected survivors - `wifi-mat alerts` - View and acknowledge alerts - `wifi-mat export` - Export data in various formats 3. REST API (wifi-densepose-mat/src/api/) - Full CRUD for disaster events - Zone management endpoints - Survivor and alert queries - WebSocket streaming for real-time updates - Comprehensive DTOs and error handling 4. WASM Build (wifi-densepose-wasm/src/mat.rs) - Browser-based disaster dashboard - Real-time survivor tracking - Zone visualization - Alert management - JavaScript API bindings 5. Detection Benchmarks (benches/detection_bench.rs) - Single survivor detection - Multi-survivor detection - Full pipeline benchmarks - Signal processing benchmarks - Hardware adapter benchmarks 6. ML Models for Debris Penetration (ml/) - DebrisModel for material analysis - VitalSignsClassifier for triage - FFT-based feature extraction - Bandpass filtering - Monte Carlo dropout for uncertainty All 134 unit tests pass. Compilation verified for: - wifi-densepose-mat - wifi-densepose-cli - wifi-densepose-wasm (with mat feature)
887 lines
28 KiB
Rust
887 lines
28 KiB
Rust
//! Axum request handlers for the MAT REST API.
|
|
//!
|
|
//! This module contains all the HTTP endpoint handlers for disaster response operations.
|
|
//! Each handler is documented with OpenAPI-style documentation comments.
|
|
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
Json,
|
|
};
|
|
use geo::Point;
|
|
use uuid::Uuid;
|
|
|
|
use super::dto::*;
|
|
use super::error::{ApiError, ApiResult};
|
|
use super::state::AppState;
|
|
use crate::domain::{
|
|
DisasterEvent, DisasterType, ScanZone, ZoneBounds,
|
|
ScanParameters, ScanResolution, MovementType,
|
|
};
|
|
|
|
// ============================================================================
|
|
// Event Handlers
|
|
// ============================================================================
|
|
|
|
/// List all disaster events.
|
|
///
|
|
/// # OpenAPI Specification
|
|
///
|
|
/// ```yaml
|
|
/// /api/v1/mat/events:
|
|
/// get:
|
|
/// summary: List disaster events
|
|
/// description: Returns a paginated list of disaster events with optional filtering
|
|
/// tags: [Events]
|
|
/// parameters:
|
|
/// - name: status
|
|
/// in: query
|
|
/// description: Filter by event status
|
|
/// schema:
|
|
/// type: string
|
|
/// enum: [Initializing, Active, Suspended, SecondarySearch, Closed]
|
|
/// - name: event_type
|
|
/// in: query
|
|
/// description: Filter by disaster type
|
|
/// schema:
|
|
/// type: string
|
|
/// - name: page
|
|
/// in: query
|
|
/// description: Page number (0-indexed)
|
|
/// schema:
|
|
/// type: integer
|
|
/// default: 0
|
|
/// - name: page_size
|
|
/// in: query
|
|
/// description: Items per page (max 100)
|
|
/// schema:
|
|
/// type: integer
|
|
/// default: 20
|
|
/// responses:
|
|
/// 200:
|
|
/// description: List of events
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/EventListResponse'
|
|
/// ```
|
|
#[tracing::instrument(skip(state))]
|
|
pub async fn list_events(
|
|
State(state): State<AppState>,
|
|
Query(query): Query<ListEventsQuery>,
|
|
) -> ApiResult<Json<EventListResponse>> {
|
|
let all_events = state.list_events();
|
|
|
|
// Apply filters
|
|
let filtered: Vec<_> = all_events
|
|
.into_iter()
|
|
.filter(|e| {
|
|
if let Some(ref status) = query.status {
|
|
let event_status: EventStatusDto = e.status().clone().into();
|
|
if !matches_status(&event_status, status) {
|
|
return false;
|
|
}
|
|
}
|
|
if let Some(ref event_type) = query.event_type {
|
|
let et: DisasterTypeDto = e.event_type().clone().into();
|
|
if et != *event_type {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
})
|
|
.collect();
|
|
|
|
let total = filtered.len();
|
|
|
|
// Apply pagination
|
|
let page_size = query.page_size.min(100).max(1);
|
|
let start = query.page * page_size;
|
|
let events: Vec<_> = filtered
|
|
.into_iter()
|
|
.skip(start)
|
|
.take(page_size)
|
|
.map(event_to_response)
|
|
.collect();
|
|
|
|
Ok(Json(EventListResponse {
|
|
events,
|
|
total,
|
|
page: query.page,
|
|
page_size,
|
|
}))
|
|
}
|
|
|
|
/// Create a new disaster event.
|
|
///
|
|
/// # OpenAPI Specification
|
|
///
|
|
/// ```yaml
|
|
/// /api/v1/mat/events:
|
|
/// post:
|
|
/// summary: Create a new disaster event
|
|
/// description: Creates a new disaster event for search and rescue operations
|
|
/// tags: [Events]
|
|
/// requestBody:
|
|
/// required: true
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/CreateEventRequest'
|
|
/// responses:
|
|
/// 201:
|
|
/// description: Event created successfully
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/EventResponse'
|
|
/// 400:
|
|
/// description: Invalid request data
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/ErrorResponse'
|
|
/// ```
|
|
#[tracing::instrument(skip(state))]
|
|
pub async fn create_event(
|
|
State(state): State<AppState>,
|
|
Json(request): Json<CreateEventRequest>,
|
|
) -> ApiResult<(StatusCode, Json<EventResponse>)> {
|
|
// Validate coordinates
|
|
if request.latitude < -90.0 || request.latitude > 90.0 {
|
|
return Err(ApiError::validation(
|
|
"Latitude must be between -90 and 90",
|
|
Some("latitude".to_string()),
|
|
));
|
|
}
|
|
if request.longitude < -180.0 || request.longitude > 180.0 {
|
|
return Err(ApiError::validation(
|
|
"Longitude must be between -180 and 180",
|
|
Some("longitude".to_string()),
|
|
));
|
|
}
|
|
|
|
let disaster_type: DisasterType = request.event_type.into();
|
|
let location = Point::new(request.longitude, request.latitude);
|
|
let mut event = DisasterEvent::new(disaster_type, location, &request.description);
|
|
|
|
// Set metadata if provided
|
|
if let Some(occupancy) = request.estimated_occupancy {
|
|
event.metadata_mut().estimated_occupancy = Some(occupancy);
|
|
}
|
|
if let Some(agency) = request.lead_agency {
|
|
event.metadata_mut().lead_agency = Some(agency);
|
|
}
|
|
|
|
let response = event_to_response(event.clone());
|
|
let event_id = *event.id().as_uuid();
|
|
state.store_event(event);
|
|
|
|
// Broadcast event creation
|
|
state.broadcast(WebSocketMessage::EventStatusChanged {
|
|
event_id,
|
|
old_status: EventStatusDto::Initializing,
|
|
new_status: response.status,
|
|
});
|
|
|
|
tracing::info!(event_id = %event_id, "Created new disaster event");
|
|
|
|
Ok((StatusCode::CREATED, Json(response)))
|
|
}
|
|
|
|
/// Get a specific disaster event by ID.
|
|
///
|
|
/// # OpenAPI Specification
|
|
///
|
|
/// ```yaml
|
|
/// /api/v1/mat/events/{event_id}:
|
|
/// get:
|
|
/// summary: Get event details
|
|
/// description: Returns detailed information about a specific disaster event
|
|
/// tags: [Events]
|
|
/// parameters:
|
|
/// - name: event_id
|
|
/// in: path
|
|
/// required: true
|
|
/// description: Event UUID
|
|
/// schema:
|
|
/// type: string
|
|
/// format: uuid
|
|
/// responses:
|
|
/// 200:
|
|
/// description: Event details
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/EventResponse'
|
|
/// 404:
|
|
/// description: Event not found
|
|
/// ```
|
|
#[tracing::instrument(skip(state))]
|
|
pub async fn get_event(
|
|
State(state): State<AppState>,
|
|
Path(event_id): Path<Uuid>,
|
|
) -> ApiResult<Json<EventResponse>> {
|
|
let event = state
|
|
.get_event(event_id)
|
|
.ok_or_else(|| ApiError::event_not_found(event_id))?;
|
|
|
|
Ok(Json(event_to_response(event)))
|
|
}
|
|
|
|
// ============================================================================
|
|
// Zone Handlers
|
|
// ============================================================================
|
|
|
|
/// List all zones for a disaster event.
|
|
///
|
|
/// # OpenAPI Specification
|
|
///
|
|
/// ```yaml
|
|
/// /api/v1/mat/events/{event_id}/zones:
|
|
/// get:
|
|
/// summary: List zones for an event
|
|
/// description: Returns all scan zones configured for a disaster event
|
|
/// tags: [Zones]
|
|
/// parameters:
|
|
/// - name: event_id
|
|
/// in: path
|
|
/// required: true
|
|
/// schema:
|
|
/// type: string
|
|
/// format: uuid
|
|
/// responses:
|
|
/// 200:
|
|
/// description: List of zones
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/ZoneListResponse'
|
|
/// 404:
|
|
/// description: Event not found
|
|
/// ```
|
|
#[tracing::instrument(skip(state))]
|
|
pub async fn list_zones(
|
|
State(state): State<AppState>,
|
|
Path(event_id): Path<Uuid>,
|
|
) -> ApiResult<Json<ZoneListResponse>> {
|
|
let event = state
|
|
.get_event(event_id)
|
|
.ok_or_else(|| ApiError::event_not_found(event_id))?;
|
|
|
|
let zones: Vec<_> = event.zones().iter().map(zone_to_response).collect();
|
|
let total = zones.len();
|
|
|
|
Ok(Json(ZoneListResponse { zones, total }))
|
|
}
|
|
|
|
/// Add a scan zone to a disaster event.
|
|
///
|
|
/// # OpenAPI Specification
|
|
///
|
|
/// ```yaml
|
|
/// /api/v1/mat/events/{event_id}/zones:
|
|
/// post:
|
|
/// summary: Add a scan zone
|
|
/// description: Creates a new scan zone within a disaster event area
|
|
/// tags: [Zones]
|
|
/// parameters:
|
|
/// - name: event_id
|
|
/// in: path
|
|
/// required: true
|
|
/// schema:
|
|
/// type: string
|
|
/// format: uuid
|
|
/// requestBody:
|
|
/// required: true
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/CreateZoneRequest'
|
|
/// responses:
|
|
/// 201:
|
|
/// description: Zone created successfully
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/ZoneResponse'
|
|
/// 404:
|
|
/// description: Event not found
|
|
/// 400:
|
|
/// description: Invalid zone configuration
|
|
/// ```
|
|
#[tracing::instrument(skip(state))]
|
|
pub async fn add_zone(
|
|
State(state): State<AppState>,
|
|
Path(event_id): Path<Uuid>,
|
|
Json(request): Json<CreateZoneRequest>,
|
|
) -> ApiResult<(StatusCode, Json<ZoneResponse>)> {
|
|
// Convert DTO to domain
|
|
let bounds = match request.bounds {
|
|
ZoneBoundsDto::Rectangle { min_x, min_y, max_x, max_y } => {
|
|
if max_x <= min_x || max_y <= min_y {
|
|
return Err(ApiError::validation(
|
|
"max coordinates must be greater than min coordinates",
|
|
Some("bounds".to_string()),
|
|
));
|
|
}
|
|
ZoneBounds::rectangle(min_x, min_y, max_x, max_y)
|
|
}
|
|
ZoneBoundsDto::Circle { center_x, center_y, radius } => {
|
|
if radius <= 0.0 {
|
|
return Err(ApiError::validation(
|
|
"radius must be positive",
|
|
Some("bounds.radius".to_string()),
|
|
));
|
|
}
|
|
ZoneBounds::circle(center_x, center_y, radius)
|
|
}
|
|
ZoneBoundsDto::Polygon { vertices } => {
|
|
if vertices.len() < 3 {
|
|
return Err(ApiError::validation(
|
|
"polygon must have at least 3 vertices",
|
|
Some("bounds.vertices".to_string()),
|
|
));
|
|
}
|
|
ZoneBounds::polygon(vertices)
|
|
}
|
|
};
|
|
|
|
let params = if let Some(p) = request.parameters {
|
|
ScanParameters {
|
|
sensitivity: p.sensitivity.clamp(0.0, 1.0),
|
|
max_depth: p.max_depth.max(0.0),
|
|
resolution: match p.resolution {
|
|
ScanResolutionDto::Quick => ScanResolution::Quick,
|
|
ScanResolutionDto::Standard => ScanResolution::Standard,
|
|
ScanResolutionDto::High => ScanResolution::High,
|
|
ScanResolutionDto::Maximum => ScanResolution::Maximum,
|
|
},
|
|
enhanced_breathing: p.enhanced_breathing,
|
|
heartbeat_detection: p.heartbeat_detection,
|
|
}
|
|
} else {
|
|
ScanParameters::default()
|
|
};
|
|
|
|
let zone = ScanZone::with_parameters(&request.name, bounds, params);
|
|
let zone_response = zone_to_response(&zone);
|
|
let zone_id = *zone.id().as_uuid();
|
|
|
|
// Add zone to event
|
|
let added = state.update_event(event_id, move |e| {
|
|
e.add_zone(zone);
|
|
true
|
|
});
|
|
|
|
if added.is_none() {
|
|
return Err(ApiError::event_not_found(event_id));
|
|
}
|
|
|
|
tracing::info!(event_id = %event_id, zone_id = %zone_id, "Added scan zone");
|
|
|
|
Ok((StatusCode::CREATED, Json(zone_response)))
|
|
}
|
|
|
|
// ============================================================================
|
|
// Survivor Handlers
|
|
// ============================================================================
|
|
|
|
/// List survivors detected in a disaster event.
|
|
///
|
|
/// # OpenAPI Specification
|
|
///
|
|
/// ```yaml
|
|
/// /api/v1/mat/events/{event_id}/survivors:
|
|
/// get:
|
|
/// summary: List survivors
|
|
/// description: Returns all detected survivors in a disaster event
|
|
/// tags: [Survivors]
|
|
/// parameters:
|
|
/// - name: event_id
|
|
/// in: path
|
|
/// required: true
|
|
/// schema:
|
|
/// type: string
|
|
/// format: uuid
|
|
/// - name: triage_status
|
|
/// in: query
|
|
/// description: Filter by triage status
|
|
/// schema:
|
|
/// type: string
|
|
/// enum: [Immediate, Delayed, Minor, Deceased, Unknown]
|
|
/// - name: zone_id
|
|
/// in: query
|
|
/// description: Filter by zone
|
|
/// schema:
|
|
/// type: string
|
|
/// format: uuid
|
|
/// - name: min_confidence
|
|
/// in: query
|
|
/// description: Minimum confidence threshold
|
|
/// schema:
|
|
/// type: number
|
|
/// - name: deteriorating_only
|
|
/// in: query
|
|
/// description: Only return deteriorating survivors
|
|
/// schema:
|
|
/// type: boolean
|
|
/// responses:
|
|
/// 200:
|
|
/// description: List of survivors
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/SurvivorListResponse'
|
|
/// 404:
|
|
/// description: Event not found
|
|
/// ```
|
|
#[tracing::instrument(skip(state))]
|
|
pub async fn list_survivors(
|
|
State(state): State<AppState>,
|
|
Path(event_id): Path<Uuid>,
|
|
Query(query): Query<ListSurvivorsQuery>,
|
|
) -> ApiResult<Json<SurvivorListResponse>> {
|
|
let event = state
|
|
.get_event(event_id)
|
|
.ok_or_else(|| ApiError::event_not_found(event_id))?;
|
|
|
|
let mut triage_summary = TriageSummary::default();
|
|
let survivors: Vec<_> = event
|
|
.survivors()
|
|
.into_iter()
|
|
.filter(|s| {
|
|
// Update triage counts for all survivors
|
|
update_triage_summary(&mut triage_summary, s.triage_status());
|
|
|
|
// Apply filters
|
|
if let Some(ref ts) = query.triage_status {
|
|
let survivor_triage: TriageStatusDto = s.triage_status().clone().into();
|
|
if !matches_triage_status(&survivor_triage, ts) {
|
|
return false;
|
|
}
|
|
}
|
|
if let Some(zone_id) = query.zone_id {
|
|
if s.zone_id().as_uuid() != &zone_id {
|
|
return false;
|
|
}
|
|
}
|
|
if let Some(min_conf) = query.min_confidence {
|
|
if s.confidence() < min_conf {
|
|
return false;
|
|
}
|
|
}
|
|
if query.deteriorating_only && !s.is_deteriorating() {
|
|
return false;
|
|
}
|
|
true
|
|
})
|
|
.map(survivor_to_response)
|
|
.collect();
|
|
|
|
let total = survivors.len();
|
|
|
|
Ok(Json(SurvivorListResponse {
|
|
survivors,
|
|
total,
|
|
triage_summary,
|
|
}))
|
|
}
|
|
|
|
// ============================================================================
|
|
// Alert Handlers
|
|
// ============================================================================
|
|
|
|
/// List alerts for a disaster event.
|
|
///
|
|
/// # OpenAPI Specification
|
|
///
|
|
/// ```yaml
|
|
/// /api/v1/mat/events/{event_id}/alerts:
|
|
/// get:
|
|
/// summary: List alerts
|
|
/// description: Returns all alerts generated for a disaster event
|
|
/// tags: [Alerts]
|
|
/// parameters:
|
|
/// - name: event_id
|
|
/// in: path
|
|
/// required: true
|
|
/// schema:
|
|
/// type: string
|
|
/// format: uuid
|
|
/// - name: priority
|
|
/// in: query
|
|
/// description: Filter by priority
|
|
/// schema:
|
|
/// type: string
|
|
/// enum: [Critical, High, Medium, Low]
|
|
/// - name: status
|
|
/// in: query
|
|
/// description: Filter by status
|
|
/// schema:
|
|
/// type: string
|
|
/// - name: pending_only
|
|
/// in: query
|
|
/// description: Only return pending alerts
|
|
/// schema:
|
|
/// type: boolean
|
|
/// - name: active_only
|
|
/// in: query
|
|
/// description: Only return active alerts
|
|
/// schema:
|
|
/// type: boolean
|
|
/// responses:
|
|
/// 200:
|
|
/// description: List of alerts
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/AlertListResponse'
|
|
/// 404:
|
|
/// description: Event not found
|
|
/// ```
|
|
#[tracing::instrument(skip(state))]
|
|
pub async fn list_alerts(
|
|
State(state): State<AppState>,
|
|
Path(event_id): Path<Uuid>,
|
|
Query(query): Query<ListAlertsQuery>,
|
|
) -> ApiResult<Json<AlertListResponse>> {
|
|
// Verify event exists
|
|
if state.get_event(event_id).is_none() {
|
|
return Err(ApiError::event_not_found(event_id));
|
|
}
|
|
|
|
let all_alerts = state.list_alerts_for_event(event_id);
|
|
let mut priority_counts = PriorityCounts::default();
|
|
|
|
let alerts: Vec<_> = all_alerts
|
|
.into_iter()
|
|
.filter(|a| {
|
|
// Update priority counts
|
|
update_priority_counts(&mut priority_counts, a.priority());
|
|
|
|
// Apply filters
|
|
if let Some(ref priority) = query.priority {
|
|
let alert_priority: PriorityDto = a.priority().into();
|
|
if !matches_priority(&alert_priority, priority) {
|
|
return false;
|
|
}
|
|
}
|
|
if let Some(ref status) = query.status {
|
|
let alert_status: AlertStatusDto = a.status().clone().into();
|
|
if !matches_alert_status(&alert_status, status) {
|
|
return false;
|
|
}
|
|
}
|
|
if query.pending_only && !a.is_pending() {
|
|
return false;
|
|
}
|
|
if query.active_only && !a.is_active() {
|
|
return false;
|
|
}
|
|
true
|
|
})
|
|
.map(|a| alert_to_response(&a))
|
|
.collect();
|
|
|
|
let total = alerts.len();
|
|
|
|
Ok(Json(AlertListResponse {
|
|
alerts,
|
|
total,
|
|
priority_counts,
|
|
}))
|
|
}
|
|
|
|
/// Acknowledge an alert.
|
|
///
|
|
/// # OpenAPI Specification
|
|
///
|
|
/// ```yaml
|
|
/// /api/v1/mat/alerts/{alert_id}/acknowledge:
|
|
/// post:
|
|
/// summary: Acknowledge an alert
|
|
/// description: Marks an alert as acknowledged by a rescue team
|
|
/// tags: [Alerts]
|
|
/// parameters:
|
|
/// - name: alert_id
|
|
/// in: path
|
|
/// required: true
|
|
/// schema:
|
|
/// type: string
|
|
/// format: uuid
|
|
/// requestBody:
|
|
/// required: true
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/AcknowledgeAlertRequest'
|
|
/// responses:
|
|
/// 200:
|
|
/// description: Alert acknowledged
|
|
/// content:
|
|
/// application/json:
|
|
/// schema:
|
|
/// $ref: '#/components/schemas/AcknowledgeAlertResponse'
|
|
/// 404:
|
|
/// description: Alert not found
|
|
/// 409:
|
|
/// description: Alert already acknowledged
|
|
/// ```
|
|
#[tracing::instrument(skip(state))]
|
|
pub async fn acknowledge_alert(
|
|
State(state): State<AppState>,
|
|
Path(alert_id): Path<Uuid>,
|
|
Json(request): Json<AcknowledgeAlertRequest>,
|
|
) -> ApiResult<Json<AcknowledgeAlertResponse>> {
|
|
let alert_data = state
|
|
.get_alert(alert_id)
|
|
.ok_or_else(|| ApiError::alert_not_found(alert_id))?;
|
|
|
|
if !alert_data.alert.is_pending() {
|
|
return Err(ApiError::InvalidState {
|
|
message: "Alert is not in pending state".to_string(),
|
|
current_state: format!("{:?}", alert_data.alert.status()),
|
|
});
|
|
}
|
|
|
|
let event_id = alert_data.event_id;
|
|
|
|
// Acknowledge the alert
|
|
state.update_alert(alert_id, |a| {
|
|
a.acknowledge(&request.acknowledged_by);
|
|
});
|
|
|
|
// Get updated alert
|
|
let updated = state
|
|
.get_alert(alert_id)
|
|
.ok_or_else(|| ApiError::alert_not_found(alert_id))?;
|
|
|
|
let response = alert_to_response(&updated.alert);
|
|
|
|
// Broadcast update
|
|
state.broadcast(WebSocketMessage::AlertUpdated {
|
|
event_id,
|
|
alert: response.clone(),
|
|
});
|
|
|
|
tracing::info!(
|
|
alert_id = %alert_id,
|
|
acknowledged_by = %request.acknowledged_by,
|
|
"Alert acknowledged"
|
|
);
|
|
|
|
Ok(Json(AcknowledgeAlertResponse {
|
|
success: true,
|
|
alert: response,
|
|
}))
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
fn event_to_response(event: DisasterEvent) -> EventResponse {
|
|
let triage_counts = event.triage_counts();
|
|
|
|
EventResponse {
|
|
id: *event.id().as_uuid(),
|
|
event_type: event.event_type().clone().into(),
|
|
status: event.status().clone().into(),
|
|
start_time: *event.start_time(),
|
|
latitude: event.location().y(),
|
|
longitude: event.location().x(),
|
|
description: event.description().to_string(),
|
|
zone_count: event.zones().len(),
|
|
survivor_count: event.survivors().len(),
|
|
triage_summary: TriageSummary {
|
|
immediate: triage_counts.immediate,
|
|
delayed: triage_counts.delayed,
|
|
minor: triage_counts.minor,
|
|
deceased: triage_counts.deceased,
|
|
unknown: triage_counts.unknown,
|
|
},
|
|
metadata: Some(EventMetadataDto {
|
|
estimated_occupancy: event.metadata().estimated_occupancy,
|
|
confirmed_rescued: event.metadata().confirmed_rescued,
|
|
confirmed_deceased: event.metadata().confirmed_deceased,
|
|
weather: event.metadata().weather.clone(),
|
|
lead_agency: event.metadata().lead_agency.clone(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn zone_to_response(zone: &ScanZone) -> ZoneResponse {
|
|
let bounds = match zone.bounds() {
|
|
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y } => {
|
|
ZoneBoundsDto::Rectangle {
|
|
min_x: *min_x,
|
|
min_y: *min_y,
|
|
max_x: *max_x,
|
|
max_y: *max_y,
|
|
}
|
|
}
|
|
ZoneBounds::Circle { center_x, center_y, radius } => {
|
|
ZoneBoundsDto::Circle {
|
|
center_x: *center_x,
|
|
center_y: *center_y,
|
|
radius: *radius,
|
|
}
|
|
}
|
|
ZoneBounds::Polygon { vertices } => {
|
|
ZoneBoundsDto::Polygon {
|
|
vertices: vertices.clone(),
|
|
}
|
|
}
|
|
};
|
|
|
|
let params = zone.parameters();
|
|
let parameters = ScanParametersDto {
|
|
sensitivity: params.sensitivity,
|
|
max_depth: params.max_depth,
|
|
resolution: match params.resolution {
|
|
ScanResolution::Quick => ScanResolutionDto::Quick,
|
|
ScanResolution::Standard => ScanResolutionDto::Standard,
|
|
ScanResolution::High => ScanResolutionDto::High,
|
|
ScanResolution::Maximum => ScanResolutionDto::Maximum,
|
|
},
|
|
enhanced_breathing: params.enhanced_breathing,
|
|
heartbeat_detection: params.heartbeat_detection,
|
|
};
|
|
|
|
ZoneResponse {
|
|
id: *zone.id().as_uuid(),
|
|
name: zone.name().to_string(),
|
|
status: zone.status().clone().into(),
|
|
bounds,
|
|
area: zone.area(),
|
|
parameters,
|
|
last_scan: zone.last_scan().cloned(),
|
|
scan_count: zone.scan_count(),
|
|
detections_count: zone.detections_count(),
|
|
}
|
|
}
|
|
|
|
fn survivor_to_response(survivor: &crate::Survivor) -> SurvivorResponse {
|
|
let location = survivor.location().map(|loc| LocationDto {
|
|
x: loc.x,
|
|
y: loc.y,
|
|
z: loc.z,
|
|
depth: loc.depth(),
|
|
uncertainty_radius: loc.uncertainty.horizontal_error,
|
|
confidence: loc.uncertainty.confidence,
|
|
});
|
|
|
|
let latest_vitals = survivor.vital_signs().latest();
|
|
let vital_signs = VitalSignsSummaryDto {
|
|
breathing_rate: latest_vitals.and_then(|v| v.breathing.as_ref().map(|b| b.rate_bpm)),
|
|
breathing_type: latest_vitals.and_then(|v| v.breathing.as_ref().map(|b| format!("{:?}", b.pattern_type))),
|
|
heart_rate: latest_vitals.and_then(|v| v.heartbeat.as_ref().map(|h| h.rate_bpm)),
|
|
has_heartbeat: latest_vitals.map(|v| v.has_heartbeat()).unwrap_or(false),
|
|
has_movement: latest_vitals.map(|v| v.has_movement()).unwrap_or(false),
|
|
movement_type: latest_vitals.and_then(|v| {
|
|
if v.movement.movement_type != MovementType::None {
|
|
Some(format!("{:?}", v.movement.movement_type))
|
|
} else {
|
|
None
|
|
}
|
|
}),
|
|
timestamp: latest_vitals.map(|v| v.timestamp).unwrap_or_else(chrono::Utc::now),
|
|
};
|
|
|
|
let metadata = {
|
|
let m = survivor.metadata();
|
|
if m.notes.is_empty() && m.tags.is_empty() && m.assigned_team.is_none() {
|
|
None
|
|
} else {
|
|
Some(SurvivorMetadataDto {
|
|
estimated_age_category: m.estimated_age_category.as_ref().map(|a| format!("{:?}", a)),
|
|
assigned_team: m.assigned_team.clone(),
|
|
notes: m.notes.clone(),
|
|
tags: m.tags.clone(),
|
|
})
|
|
}
|
|
};
|
|
|
|
SurvivorResponse {
|
|
id: *survivor.id().as_uuid(),
|
|
zone_id: *survivor.zone_id().as_uuid(),
|
|
status: survivor.status().clone().into(),
|
|
triage_status: survivor.triage_status().clone().into(),
|
|
location,
|
|
vital_signs,
|
|
confidence: survivor.confidence(),
|
|
first_detected: *survivor.first_detected(),
|
|
last_updated: *survivor.last_updated(),
|
|
is_deteriorating: survivor.is_deteriorating(),
|
|
metadata,
|
|
}
|
|
}
|
|
|
|
fn alert_to_response(alert: &crate::Alert) -> AlertResponse {
|
|
let location = alert.payload().location.as_ref().map(|loc| LocationDto {
|
|
x: loc.x,
|
|
y: loc.y,
|
|
z: loc.z,
|
|
depth: loc.depth(),
|
|
uncertainty_radius: loc.uncertainty.horizontal_error,
|
|
confidence: loc.uncertainty.confidence,
|
|
});
|
|
|
|
AlertResponse {
|
|
id: *alert.id().as_uuid(),
|
|
survivor_id: *alert.survivor_id().as_uuid(),
|
|
priority: alert.priority().into(),
|
|
status: alert.status().clone().into(),
|
|
title: alert.payload().title.clone(),
|
|
message: alert.payload().message.clone(),
|
|
triage_status: alert.payload().triage_status.clone().into(),
|
|
location,
|
|
recommended_action: if alert.payload().recommended_action.is_empty() {
|
|
None
|
|
} else {
|
|
Some(alert.payload().recommended_action.clone())
|
|
},
|
|
created_at: *alert.created_at(),
|
|
acknowledged_at: alert.acknowledged_at().cloned(),
|
|
acknowledged_by: alert.acknowledged_by().map(String::from),
|
|
escalation_count: alert.escalation_count(),
|
|
}
|
|
}
|
|
|
|
fn update_triage_summary(summary: &mut TriageSummary, status: &crate::TriageStatus) {
|
|
match status {
|
|
crate::TriageStatus::Immediate => summary.immediate += 1,
|
|
crate::TriageStatus::Delayed => summary.delayed += 1,
|
|
crate::TriageStatus::Minor => summary.minor += 1,
|
|
crate::TriageStatus::Deceased => summary.deceased += 1,
|
|
crate::TriageStatus::Unknown => summary.unknown += 1,
|
|
}
|
|
}
|
|
|
|
fn update_priority_counts(counts: &mut PriorityCounts, priority: crate::Priority) {
|
|
match priority {
|
|
crate::Priority::Critical => counts.critical += 1,
|
|
crate::Priority::High => counts.high += 1,
|
|
crate::Priority::Medium => counts.medium += 1,
|
|
crate::Priority::Low => counts.low += 1,
|
|
}
|
|
}
|
|
|
|
// Match helper functions (avoiding PartialEq on DTOs for flexibility)
|
|
fn matches_status(a: &EventStatusDto, b: &EventStatusDto) -> bool {
|
|
std::mem::discriminant(a) == std::mem::discriminant(b)
|
|
}
|
|
|
|
fn matches_triage_status(a: &TriageStatusDto, b: &TriageStatusDto) -> bool {
|
|
std::mem::discriminant(a) == std::mem::discriminant(b)
|
|
}
|
|
|
|
fn matches_priority(a: &PriorityDto, b: &PriorityDto) -> bool {
|
|
std::mem::discriminant(a) == std::mem::discriminant(b)
|
|
}
|
|
|
|
fn matches_alert_status(a: &AlertStatusDto, b: &AlertStatusDto) -> bool {
|
|
std::mem::discriminant(a) == std::mem::discriminant(b)
|
|
}
|