import { useRef } from "react"; import type { OrbitPassInfo } from "../tgu-map-2d/geometry/passes"; type MapPassSelectorProps = { passes: OrbitPassInfo[]; fromIndex: number; toIndex: number; onFromChange: (idx: number) => void; onToChange: (idx: number) => void; }; function localHM(ms: number): string { const d = new Date(ms); return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; } function localDM(ms: number): string { const d = new Date(ms); return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}`; } export function MapPassSelector({ passes, fromIndex, toIndex, onFromChange, onToChange }: MapPassSelectorProps) { const trackRef = useRef(null); const n = passes.length; if (n === 0) return null; const fromPass = passes[Math.max(0, Math.min(fromIndex - 1, n - 1))]; const toPass = passes[Math.max(0, Math.min(toIndex - 1, n - 1))]; const multiDay = localDM(passes[0].startMs) !== localDM(passes[n - 1].startMs); const isFullRange = fromIndex === 1 && toIndex === n; const fromRev = fromPass.revolution ?? fromIndex; const toRev = toPass.revolution ?? toIndex; const pct = (idx: number): number => (n <= 1 ? 0 : ((idx - 1) / (n - 1)) * 100); const fromPct = pct(fromIndex); const toPct = pct(toIndex); const indexAt = (clientX: number): number => { const rect = trackRef.current?.getBoundingClientRect(); if (!rect || n <= 1) return 1; return Math.round(Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)) * (n - 1)) + 1; }; // Какой ползунок реально тянем. При наложении (fromIndex === toIndex) хват попадает на верхний // (to), но на правом краю он заперт fromIndex и не может уйти влево — поэтому при наложении // выбираем ползунок по направлению движения: влево → from (открыть диапазон), вправо → to. const dragRef = useRef<"from" | "to" | null>(null); const makeHandlers = (which: "from" | "to") => ({ onPointerDown(e: React.PointerEvent) { e.stopPropagation(); dragRef.current = which; (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId); }, onPointerMove(e: React.PointerEvent) { if (!(e.currentTarget as HTMLDivElement).hasPointerCapture(e.pointerId)) return; const idx = indexAt(e.clientX); let active = dragRef.current; if (fromIndex === toIndex) { if (idx < fromIndex) active = "from"; else if (idx > toIndex) active = "to"; dragRef.current = active; } if (active === "from") { onFromChange(Math.max(1, Math.min(idx, toIndex))); } else { onToChange(Math.min(n, Math.max(idx, fromIndex))); } }, onPointerUp(e: React.PointerEvent) { dragRef.current = null; (e.currentTarget as HTMLDivElement).releasePointerCapture(e.pointerId); }, }); const onTrackPointerDown = (e: React.PointerEvent) => { const idx = indexAt(e.clientX); if (Math.abs(idx - fromIndex) <= Math.abs(idx - toIndex)) { onFromChange(Math.max(1, Math.min(idx, toIndex))); } else { onToChange(Math.min(n, Math.max(idx, fromIndex))); } }; return (
Витки {fromRev}–{toRev} {multiDay ? `${localDM(fromPass.startMs)} ` : ""}{localHM(fromPass.startMs)} {" – "} {multiDay ? `${localDM(toPass.startMs)} ` : ""}{localHM(toPass.startMs)} {!isFullRange && ( )}
{/* Track line */}
{/* Tick marks */} {passes.map((p) => (
= fromIndex && p.index <= toIndex ? " is-active" : ""}`} style={{ left: `${pct(p.index)}%` }} /> ))} {/* From handle */}
{/* To handle */}
); }