feat(tgu-ops-ui): display real ground track and swath from ballistics service
Replace synthetic orbit calculation (Kepler + spacecraft ID hash) with real
data from pcp-ballistics-service via a new proxy endpoint in pcp-ui-service.
Backend:
- TguPlanningController: add GET /api/tgu-planning/spacecraft/{noradId}/flight-line
that proxies FlightLineDTO[] from BallisticsService for a given time interval
Frontend:
- Fetch FlightLineDTO[] (revolution, time, lat/long, swath boundaries) using
the spacecraft's noradId for the current 24h map window
- Group points by real revolution number to build OrbitPassInfo[] with actual
orbit numbers and UTC times instead of synthetic Kepler-based estimates
- Build ground track MapLines and swath MapPolygons from real coordinates;
swath polygon = outer-left edge forward + outer-right edge reversed
- Enable swath layer in the editor map; draw with teal fill/stroke matching
the track colour
- MapPassSelector now shows real revolution numbers in header and handle labels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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 utcHM(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function utcDM(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
return `${String(d.getUTCDate()).padStart(2, "0")}.${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function MapPassSelector({ passes, fromIndex, toIndex, onFromChange, onToChange }: MapPassSelectorProps) {
|
||||
const trackRef = useRef<HTMLDivElement>(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 = utcDM(passes[0].startMs) !== utcDM(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;
|
||||
};
|
||||
|
||||
const makeHandlers = (isFrom: boolean) => ({
|
||||
onPointerDown(e: React.PointerEvent<HTMLDivElement>) {
|
||||
e.stopPropagation();
|
||||
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
|
||||
},
|
||||
onPointerMove(e: React.PointerEvent<HTMLDivElement>) {
|
||||
if (!(e.currentTarget as HTMLDivElement).hasPointerCapture(e.pointerId)) return;
|
||||
const idx = indexAt(e.clientX);
|
||||
if (isFrom) {
|
||||
onFromChange(Math.max(1, Math.min(idx, toIndex)));
|
||||
} else {
|
||||
onToChange(Math.min(n, Math.max(idx, fromIndex)));
|
||||
}
|
||||
},
|
||||
onPointerUp(e: React.PointerEvent<HTMLDivElement>) {
|
||||
(e.currentTarget as HTMLDivElement).releasePointerCapture(e.pointerId);
|
||||
},
|
||||
});
|
||||
|
||||
const onTrackPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
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 (
|
||||
<div className="map-pass-selector">
|
||||
<div className="map-pass-selector__header">
|
||||
<span className="map-pass-selector__title">Витки</span>
|
||||
<span className="map-pass-selector__nums">{fromRev}–{toRev}</span>
|
||||
<span className="map-pass-selector__times">
|
||||
{multiDay ? `${utcDM(fromPass.startMs)} ` : ""}{utcHM(fromPass.startMs)}
|
||||
{" – "}
|
||||
{multiDay ? `${utcDM(toPass.startMs)} ` : ""}{utcHM(toPass.startMs)}
|
||||
{" UTC"}
|
||||
</span>
|
||||
{!isFullRange && (
|
||||
<button
|
||||
type="button"
|
||||
className="map-pass-selector__reset"
|
||||
title="Все витки"
|
||||
onClick={() => { onFromChange(1); onToChange(n); }}
|
||||
>
|
||||
Все
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="map-pass-selector__body" onPointerDown={onTrackPointerDown}>
|
||||
{/* Track line */}
|
||||
<div className="map-pass-selector__track" ref={trackRef}>
|
||||
<div
|
||||
className="map-pass-selector__fill"
|
||||
style={{ left: `${fromPct}%`, width: `${toPct - fromPct}%` }}
|
||||
/>
|
||||
{/* Tick marks */}
|
||||
{passes.map((p) => (
|
||||
<div
|
||||
key={p.index}
|
||||
className={`map-pass-selector__tick${p.index >= fromIndex && p.index <= toIndex ? " is-active" : ""}`}
|
||||
style={{ left: `${pct(p.index)}%` }}
|
||||
/>
|
||||
))}
|
||||
{/* From handle */}
|
||||
<div
|
||||
className="map-pass-selector__thumb"
|
||||
style={{ left: `${fromPct}%` }}
|
||||
{...makeHandlers(true)}
|
||||
/>
|
||||
{/* To handle */}
|
||||
<div
|
||||
className="map-pass-selector__thumb"
|
||||
style={{ left: `${toPct}%` }}
|
||||
{...makeHandlers(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Labels below handles */}
|
||||
<div className="map-pass-selector__labels">
|
||||
<div className="map-pass-selector__label" style={{ left: `${fromPct}%` }}>
|
||||
<span className="map-pass-selector__label-num">{fromRev}</span>
|
||||
<span className="map-pass-selector__label-time">{multiDay && `${utcDM(fromPass.startMs)} `}{utcHM(fromPass.startMs)}</span>
|
||||
</div>
|
||||
{toIndex !== fromIndex && (
|
||||
<div className="map-pass-selector__label map-pass-selector__label--right" style={{ left: `${toPct}%` }}>
|
||||
<span className="map-pass-selector__label-num">{toRev}</span>
|
||||
<span className="map-pass-selector__label-time">{multiDay && `${utcDM(toPass.startMs)} `}{utcHM(toPass.startMs)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user