import type { FleetTelemetryPayload, HazardType } from "./api"; export type FleetHazardType = Exclude; export interface FleetDetection { hazard_type: FleetHazardType; confidence: number; bbox: [number, number, number, number]; } export interface FleetLocation { lat: number; lon: number; speed: number | null; at: number; } export interface LastFleetReport { lat: number; lon: number; at: number; } export const FLEET_CONFIDENCE = 0.55; export const TRAFFIC_BLOCK_CONFIDENCE = 0.45; export const TRAFFIC_BLOCK_SPEED_MPS = 2; export const TRAFFIC_BLOCK_DWELL_MS = 10_000; export const FLEET_DEBOUNCE_METERS = 10; const EARTH_RADIUS_M = 6_371_000; export function haversineMeters(a: Pick, b: Pick): number { const dLat = ((b.lat - a.lat) * Math.PI) / 180; const dLon = ((b.lon - a.lon) * Math.PI) / 180; const lat1 = (a.lat * Math.PI) / 180; const lat2 = (b.lat * Math.PI) / 180; const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2; return 2 * EARTH_RADIUS_M * Math.asin(Math.sqrt(h)); } export function createClientEventId(): string { if ("randomUUID" in crypto) return crypto.randomUUID(); return `fleet-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; } export function shouldSendFleetReport( detection: FleetDetection, location: FleetLocation, lastReports: Map, ): boolean { const previous = lastReports.get(detection.hazard_type); if (!previous) return true; return haversineMeters(previous, location) > FLEET_DEBOUNCE_METERS; } export function bboxAreaFraction(bbox: [number, number, number, number], frameSize: number): number { const [x1, y1, x2, y2] = bbox; const w = Math.max(0, x2 - x1); const h = Math.max(0, y2 - y1); return Math.min(1, (w * h) / (frameSize * frameSize)); } export function toFleetPayload(opts: { detection: FleetDetection; location: FleetLocation; thumbnailB64?: string; frameSize: number; }): FleetTelemetryPayload { return { lat: opts.location.lat, lon: opts.location.lon, hazard_type: opts.detection.hazard_type, confidence: opts.detection.confidence, timestamp: new Date().toISOString(), speed: opts.location.speed ?? undefined, bbox: opts.detection.bbox, bbox_area_frac: bboxAreaFraction(opts.detection.bbox, opts.frameSize), thumbnail_b64: opts.thumbnailB64, client_event_id: createClientEventId(), }; }