import type { Contact, RadioConfig, MessagePath } from '../types';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from './ui/dialog';
import { Button } from './ui/button';
import {
resolvePath,
calculateDistance,
isValidLocation,
formatDistance,
type SenderInfo,
type ResolvedPath,
type PathHop,
} from '../utils/pathUtils';
import { formatTime } from '../utils/messageParser';
import { getMapFocusHash } from '../utils/urlHash';
interface PathModalProps {
open: boolean;
onClose: () => void;
paths: MessagePath[];
senderInfo: SenderInfo;
contacts: Contact[];
config: RadioConfig | null;
}
export function PathModal({ open, onClose, paths, senderInfo, contacts, config }: PathModalProps) {
// Resolve all paths
const resolvedPaths = paths.map((p) => ({
...p,
resolved: resolvePath(p.path, senderInfo, contacts, config),
}));
const hasSinglePath = paths.length === 1;
return (
);
}
interface PathVisualizationProps {
resolved: ResolvedPath;
senderInfo: SenderInfo;
/** If true, hide the straight-line distance (shown once at container level for multi-path) */
hideStraightLine?: boolean;
}
function PathVisualization({ resolved, senderInfo, hideStraightLine }: 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:{' '}
{resolved.hasGaps ? '>' : ''}
{formatDistance(resolved.totalDistances[0])}
)}
{/* Straight-line distance (when both sender and receiver have coordinates) */}
{!hideStraightLine &&
isValidLocation(resolved.sender.lat, resolved.sender.lon) &&
isValidLocation(resolved.receiver.lat, resolved.receiver.lon) && (
0
? 'pt-1'
: 'pt-3 mt-3 border-t border-border'
}
>
Straight-line distance:
{formatDistance(
calculateDistance(
resolved.sender.lat,
resolved.sender.lon,
resolved.receiver.lat,
resolved.receiver.lon
)!
)}
)}
);
}
interface PathNodeProps {
label: string;
name: string;
prefix: string;
distance: number | null;
isFirst?: boolean;
isLast?: boolean;
/** Optional coordinates for map link */
lat?: number | null;
lon?: number | null;
/** Public key for map focus link (required if lat/lon provided) */
publicKey?: string;
}
function PathNode({
label,
name,
prefix,
distance,
isFirst,
isLast,
lat,
lon,
publicKey,
}: PathNodeProps) {
const hasLocation = isValidLocation(lat ?? null, lon ?? null) && publicKey;
return (
{/* Vertical line and dot column */}
{!isFirst &&
}
{!isLast &&
}
{/* Content */}
{label}
{name} ({prefix})
{distance !== null && (
- {formatDistance(distance)}
)}
{hasLocation && }
);
}
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);
const hasLocation = isValidLocation(contact.lat, contact.lon);
return (
{contact.name || contact.public_key.slice(0, 12)}{' '}
({contact.public_key.slice(0, 2).toUpperCase()})
{dist !== null && (
- {formatDistance(dist)}
)}
{hasLocation && (
)}
);
})}
) : (
{hop.matches[0].name || hop.matches[0].public_key.slice(0, 12)}{' '}
({hop.prefix})
{hop.distanceFromPrev !== null && (
- {formatDistance(hop.distanceFromPrev)}
)}
{isValidLocation(hop.matches[0].lat, hop.matches[0].lon) && (
)}
)}
);
}
/**
* Render clickable coordinates that open the map focused on the contact
*/
function CoordinateLink({ lat, lon, publicKey }: { lat: number; lon: number; publicKey: string }) {
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// Open map in new tab with focus on this contact
const url = window.location.origin + window.location.pathname + getMapFocusHash(publicKey);
window.open(url, '_blank');
};
return (
({lat.toFixed(4)}, {lon.toFixed(4)})
);
}
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);
}