Some checks failed
Deploy KCG / deploy (push) Failing after 1m43s
Co-authored-by: htlee <htlee@gcsc.co.kr> Co-committed-by: htlee <htlee@gcsc.co.kr>
184 lines
6.7 KiB
TypeScript
184 lines
6.7 KiB
TypeScript
import { memo, useMemo, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Marker, Popup, Source, Layer } from 'react-map-gl/maplibre';
|
|
import type { SatellitePosition } from '../../types';
|
|
|
|
interface Props {
|
|
satellites: SatellitePosition[];
|
|
}
|
|
|
|
const CAT_COLORS: Record<SatellitePosition['category'], string> = {
|
|
reconnaissance: '#ef4444',
|
|
communications: '#3b82f6',
|
|
navigation: '#22c55e',
|
|
weather: '#a855f7',
|
|
other: '#6b7280',
|
|
};
|
|
|
|
const CAT_LABELS: Record<SatellitePosition['category'], string> = {
|
|
reconnaissance: 'RECON', communications: 'COMMS',
|
|
navigation: 'NAV', weather: 'WX', other: 'SAT',
|
|
};
|
|
|
|
const SVG_RECON = (
|
|
<>
|
|
<rect x={8} y={6} width={8} height={12} rx={1.5} fill="currentColor" opacity={0.9} />
|
|
<rect x={1} y={8} width={6} height={3} rx={0.5} fill="currentColor" opacity={0.7} />
|
|
<rect x={17} y={8} width={6} height={3} rx={0.5} fill="currentColor" opacity={0.7} />
|
|
<circle cx={12} cy={16} r={1.8} fill="none" stroke="currentColor" strokeWidth={1} opacity={0.6} />
|
|
<circle cx={12} cy={16} r={0.6} fill="currentColor" opacity={0.6} />
|
|
</>
|
|
);
|
|
|
|
const SVG_COMMS = (
|
|
<>
|
|
<rect x={9} y={7} width={6} height={10} rx={1} fill="currentColor" opacity={0.9} />
|
|
<rect x={1} y={9} width={7} height={2.5} rx={0.5} fill="currentColor" opacity={0.7} />
|
|
<rect x={16} y={9} width={7} height={2.5} rx={0.5} fill="currentColor" opacity={0.7} />
|
|
<path d="M10 4 Q12 2 14 4" fill="none" stroke="currentColor" strokeWidth={1.2} opacity={0.8} />
|
|
<line x1={12} y1={4} x2={12} y2={7} stroke="currentColor" strokeWidth={0.8} />
|
|
</>
|
|
);
|
|
|
|
const SVG_NAV = (
|
|
<>
|
|
<rect x={9} y={6} width={6} height={12} rx={1} fill="currentColor" opacity={0.9} />
|
|
<rect x={2} y={8} width={6} height={2.5} rx={0.5} fill="currentColor" opacity={0.7} />
|
|
<rect x={16} y={8} width={6} height={2.5} rx={0.5} fill="currentColor" opacity={0.7} />
|
|
<line x1={12} y1={3} x2={12} y2={6} stroke="currentColor" strokeWidth={1} />
|
|
<circle cx={12} cy={2.5} r={1} fill="currentColor" opacity={0.7} />
|
|
</>
|
|
);
|
|
|
|
const SVG_WEATHER = (
|
|
<>
|
|
<rect x={8} y={7} width={8} height={10} rx={1.5} fill="currentColor" opacity={0.9} />
|
|
<rect x={1} y={9} width={6} height={2.5} rx={0.5} fill="currentColor" opacity={0.7} />
|
|
<rect x={17} y={9} width={6} height={2.5} rx={0.5} fill="currentColor" opacity={0.7} />
|
|
<circle cx={12} cy={12} r={2.5} fill="none" stroke="currentColor" strokeWidth={0.8} opacity={0.5} />
|
|
</>
|
|
);
|
|
|
|
const SVG_OTHER = (
|
|
<>
|
|
<rect x={9} y={7} width={6} height={10} rx={1} fill="currentColor" opacity={0.9} />
|
|
<rect x={2} y={9} width={6} height={2.5} rx={0.5} fill="currentColor" opacity={0.7} />
|
|
<rect x={16} y={9} width={6} height={2.5} rx={0.5} fill="currentColor" opacity={0.7} />
|
|
</>
|
|
);
|
|
|
|
const SVG_MAP: Record<SatellitePosition['category'], React.ReactNode> = {
|
|
reconnaissance: SVG_RECON, communications: SVG_COMMS,
|
|
navigation: SVG_NAV, weather: SVG_WEATHER, other: SVG_OTHER,
|
|
};
|
|
|
|
export function SatelliteLayer({ satellites }: Props) {
|
|
const trackData = useMemo(() => {
|
|
const features: GeoJSON.Feature[] = [];
|
|
for (const sat of satellites) {
|
|
if (!sat.groundTrack || sat.groundTrack.length < 2) continue;
|
|
const color = CAT_COLORS[sat.category];
|
|
let segment: [number, number][] = [];
|
|
for (let i = 0; i < sat.groundTrack.length; i++) {
|
|
const [lat, lng] = sat.groundTrack[i];
|
|
if (i > 0) {
|
|
const [, prevLng] = sat.groundTrack[i - 1];
|
|
if (Math.abs(lng - prevLng) > 180) {
|
|
if (segment.length > 1) {
|
|
features.push({
|
|
type: 'Feature',
|
|
properties: { color },
|
|
geometry: { type: 'LineString', coordinates: segment.map(([la, lo]) => [lo, la]) },
|
|
});
|
|
}
|
|
segment = [];
|
|
}
|
|
}
|
|
segment.push([lat, lng]);
|
|
}
|
|
if (segment.length > 1) {
|
|
features.push({
|
|
type: 'Feature',
|
|
properties: { color },
|
|
geometry: { type: 'LineString', coordinates: segment.map(([la, lo]) => [lo, la]) },
|
|
});
|
|
}
|
|
}
|
|
return { type: 'FeatureCollection' as const, features };
|
|
}, [satellites]);
|
|
|
|
return (
|
|
<>
|
|
{trackData.features.length > 0 && (
|
|
<Source id="satellite-tracks" type="geojson" data={trackData}>
|
|
<Layer
|
|
id="satellite-track-lines"
|
|
type="line"
|
|
paint={{
|
|
'line-color': ['get', 'color'],
|
|
'line-width': 1,
|
|
'line-opacity': 0.25,
|
|
'line-dasharray': [6, 4],
|
|
}}
|
|
/>
|
|
</Source>
|
|
)}
|
|
{satellites.map(sat => (
|
|
<SatelliteMarker key={sat.noradId} sat={sat} />
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
const SatelliteMarker = memo(function SatelliteMarker({ sat }: { sat: SatellitePosition }) {
|
|
const [showPopup, setShowPopup] = useState(false);
|
|
const { t } = useTranslation();
|
|
const color = CAT_COLORS[sat.category];
|
|
const svgBody = SVG_MAP[sat.category];
|
|
const size = 22;
|
|
|
|
return (
|
|
<>
|
|
<Marker longitude={sat.lng} latitude={sat.lat} anchor="center">
|
|
<div className="relative">
|
|
<div
|
|
style={{ color }} className="size-[22px] cursor-pointer"
|
|
onClick={(e) => { e.stopPropagation(); setShowPopup(true); }}
|
|
>
|
|
<svg viewBox="0 0 24 24" width={size} height={size} style={{ color }}>
|
|
{svgBody}
|
|
</svg>
|
|
</div>
|
|
<div className="gl-marker-label" style={{ color, fontSize: 10 }}>
|
|
{sat.name}
|
|
</div>
|
|
</div>
|
|
</Marker>
|
|
{showPopup && (
|
|
<Popup longitude={sat.lng} latitude={sat.lat}
|
|
onClose={() => setShowPopup(false)} closeOnClick={false}
|
|
anchor="bottom" maxWidth="200px" className="gl-popup">
|
|
<div className="min-w-[180px] font-mono text-xs">
|
|
<div className="mb-1.5 flex items-center gap-2">
|
|
<span style={{
|
|
background: color,
|
|
}} className="rounded-sm px-1.5 py-px text-[10px] font-bold text-black">
|
|
{CAT_LABELS[sat.category]}
|
|
</span>
|
|
<strong>{sat.name}</strong>
|
|
</div>
|
|
<table className="w-full text-[11px]">
|
|
<tbody>
|
|
<tr><td className="text-kcg-muted">{t('satellite.norad')}</td><td>{sat.noradId}</td></tr>
|
|
<tr><td className="text-kcg-muted">{t('satellite.lat')}</td><td>{sat.lat.toFixed(2)}°</td></tr>
|
|
<tr><td className="text-kcg-muted">{t('satellite.lng')}</td><td>{sat.lng.toFixed(2)}°</td></tr>
|
|
<tr><td className="text-kcg-muted">{t('satellite.alt')}</td><td>{Math.round(sat.altitude)} km</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Popup>
|
|
)}
|
|
</>
|
|
);
|
|
});
|