import React, { useRef, useState, useCallback, useEffect } from 'react'; import { useMapTransform } from '../../contexts/MapTransformContext'; import { pxToWorld, formatCoord } from '../../utils/coordinates'; import { PlayerDots } from './PlayerDots'; import type { TelemetrySnapshot } from '../../types'; interface Props { players: TelemetrySnapshot[]; getColor: (name: string) => string; onSelectPlayer: (name: string) => void; } export const MapView: React.FC = ({ players, getColor, onSelectPlayer }) => { const containerRef = useRef(null); const { transform, dispatch } = useMapTransform(); const [imgSize, setImgSize] = useState({ w: 0, h: 0 }); const [tooltip, setTooltip] = useState<{ x: number; y: number; player: TelemetrySnapshot } | null>(null); const [worldCoord, setWorldCoord] = useState<{ ns: number; ew: number } | null>(null); const dragRef = useRef<{ dragging: boolean; sx: number; sy: number; startOffX: number; startOffY: number }>({ dragging: false, sx: 0, sy: 0, startOffX: 0, startOffY: 0, }); const onImgLoad = useCallback((e: React.SyntheticEvent) => { const img = e.currentTarget; setImgSize({ w: img.naturalWidth, h: img.naturalHeight }); // Fit map to container on first load if (containerRef.current) { const cw = containerRef.current.clientWidth; const ch = containerRef.current.clientHeight; const scale = Math.min(cw / img.naturalWidth, ch / img.naturalHeight); dispatch({ type: 'SET', scale, offX: (cw - img.naturalWidth * scale) / 2, offY: (ch - img.naturalHeight * scale) / 2 }); } }, [dispatch]); // Wheel zoom const onWheel = useCallback((e: React.WheelEvent) => { e.preventDefault(); const rect = containerRef.current?.getBoundingClientRect(); if (!rect) return; const factor = e.deltaY < 0 ? 1.1 : 0.9; dispatch({ type: 'ZOOM', factor, cx: e.clientX - rect.left, cy: e.clientY - rect.top }); }, [dispatch]); // Pan drag const onMouseDown = useCallback((e: React.MouseEvent) => { if (e.button !== 0) return; dragRef.current = { dragging: true, sx: e.clientX, sy: e.clientY, startOffX: transform.offX, startOffY: transform.offY }; }, [transform.offX, transform.offY]); useEffect(() => { const onMouseMove = (e: MouseEvent) => { const d = dragRef.current; if (d.dragging) { dispatch({ type: 'SET', scale: transform.scale, offX: d.startOffX + (e.clientX - d.sx), offY: d.startOffY + (e.clientY - d.sy) }); } // World coordinate display if (containerRef.current && imgSize.w > 0) { const rect = containerRef.current.getBoundingClientRect(); const coord = pxToWorld(e.clientX - rect.left, e.clientY - rect.top, transform.scale, transform.offX, transform.offY, imgSize.w, imgSize.h); setWorldCoord(coord); } }; const onMouseUp = () => { dragRef.current.dragging = false; }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); return () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; }, [dispatch, transform.scale, transform.offX, transform.offY, imgSize.w, imgSize.h]); const handleDotHover = useCallback((player: TelemetrySnapshot | null, x: number, y: number) => { setTooltip(player ? { x, y, player } : null); }, []); return (
Dereth {imgSize.w > 0 && ( )}
{/* Tooltip */} {tooltip && (
{tooltip.player.character_name}
{formatCoord(tooltip.player.ns, tooltip.player.ew)}
{tooltip.player.kills_per_hour} kph · {tooltip.player.kills?.toLocaleString()} kills
)} {/* Coordinate display */} {worldCoord && (
{formatCoord(worldCoord.ns, worldCoord.ew)}
)}
); };