import type { Contact, RadioConfig } from '../types'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from './ui/dialog'; import { Button } from './ui/button'; import { resolvePath, calculateDistance, isValidLocation, type SenderInfo, type ResolvedPath, type PathHop, } from '../utils/pathUtils'; interface PathModalProps { open: boolean; onClose: () => void; path: string; senderInfo: SenderInfo; contacts: Contact[]; config: RadioConfig | null; } export function PathModal({ open, onClose, path, senderInfo, contacts, config }: PathModalProps) { const resolved = resolvePath(path, senderInfo, contacts, config); return ( !isOpen && onClose()}> Message Path This shows one route that this message traveled through the mesh network. Flood messages may arrive via multiple paths, and routers may be incorrectly identified due to prefix collisions between heard and non-heard router advertisements.
); } interface PathVisualizationProps { resolved: ResolvedPath; } function PathVisualization({ resolved }: PathVisualizationProps) { // Track previous location for each hop to calculate distances // Returns null if previous hop was ambiguous or has invalid location const getPrevLocation = (hopIndex: number): { lat: number | null; lon: number | null } | null => { if (hopIndex === 0) { // Check if sender has valid location if (!isValidLocation(resolved.sender.lat, resolved.sender.lon)) { return null; } return { lat: resolved.sender.lat, lon: resolved.sender.lon }; } const prevHop = resolved.hops[hopIndex - 1]; // If previous hop was ambiguous, we can't show meaningful distances if (prevHop.matches.length > 1) { return null; } // If previous hop was unknown, we also can't calculate if (prevHop.matches.length === 0) { return null; } // Check if previous hop has valid location if (isValidLocation(prevHop.matches[0].lat, prevHop.matches[0].lon)) { return { lat: prevHop.matches[0].lat, lon: prevHop.matches[0].lon }; } return null; }; return (
{/* Sender */} {/* Hops */} {resolved.hops.map((hop, index) => ( ))} {/* Receiver */} {/* Total distance */} {resolved.totalDistances && resolved.totalDistances.length > 0 && (
Presumed unambiguous distance covered:{' '} {formatDistance(resolved.totalDistances[0])}
)}
); } interface PathNodeProps { label: string; name: string; prefix: string; distance: number | null; isFirst?: boolean; isLast?: boolean; } function PathNode({ label, name, prefix, distance, isFirst, isLast }: PathNodeProps) { return (
{/* Vertical line and dot column */}
{!isFirst &&
}
{!isLast &&
}
{/* Content */}
{label}
{name} ({prefix})
{distance !== null && (
{formatDistance(distance)}
)}
); } interface HopNodeProps { hop: PathHop; hopNumber: number; prevLocation: { lat: number | null; lon: number | null } | null; } function HopNode({ hop, hopNumber, prevLocation }: HopNodeProps) { const isAmbiguous = hop.matches.length > 1; const isUnknown = hop.matches.length === 0; // Calculate distance from previous location for a contact // Returns null if prev location unknown/ambiguous or contact has no valid location const getDistanceForContact = (contact: { lat: number | null; lon: number | null; }): number | null => { if (!prevLocation || prevLocation.lat === null || prevLocation.lon === null) { return null; } // Check if contact has valid location if (!isValidLocation(contact.lat, contact.lon)) { return null; } return calculateDistance(prevLocation.lat, prevLocation.lon, contact.lat, contact.lon); }; return (
{/* Vertical line and dot column */}
{/* Content */}
Hop {hopNumber} {isAmbiguous && (ambiguous)}
{isUnknown ? (
<UNKNOWN {hop.prefix}>
) : isAmbiguous ? (
{hop.matches.map((contact) => { const dist = getDistanceForContact(contact); return (
{contact.name || contact.public_key.slice(0, 12)}{' '} ({contact.public_key.slice(0, 2).toUpperCase()}) {dist !== null && ( - {formatDistance(dist)} )}
); })}
) : (
{hop.matches[0].name || hop.matches[0].public_key.slice(0, 12)}{' '} ({hop.prefix}) {hop.distanceFromPrev !== null && ( - {formatDistance(hop.distanceFromPrev)} )}
)}
); } function formatDistance(km: number): string { if (km < 1) { return `${Math.round(km * 1000)}m`; } return `${km.toFixed(1)}km`; } function calculateReceiverDistance(resolved: ResolvedPath): number | null { // Get last hop's location (if any) let prevLat: number | null = null; let prevLon: number | null = null; if (resolved.hops.length > 0) { const lastHop = resolved.hops[resolved.hops.length - 1]; // Only use last hop if it's unambiguous and has valid location if ( lastHop.matches.length === 1 && isValidLocation(lastHop.matches[0].lat, lastHop.matches[0].lon) ) { prevLat = lastHop.matches[0].lat; prevLon = lastHop.matches[0].lon; } } else { // No hops, calculate from sender to receiver (if sender has valid location) if (isValidLocation(resolved.sender.lat, resolved.sender.lon)) { prevLat = resolved.sender.lat; prevLon = resolved.sender.lon; } } if (prevLat === null || prevLon === null) { return null; } // Check receiver has valid location if (!isValidLocation(resolved.receiver.lat, resolved.receiver.lon)) { return null; } return calculateDistance(prevLat, prevLon, resolved.receiver.lat, resolved.receiver.lon); }