fix: 항적 토글 + 패널 레이아웃 + TripsLayer 색상 분리
항적 토글 역할 변경: - 항상 ON: TripsLayer (애니메이션 트레일) + 센터 트레일 + 도트 - "항적" 토글: 전체 24h 정적 항적 PathLayer (멤버 + 연관 선박) ON 시 회색/연파랑 배경 경로 위에 고채도 TripsLayer 애니메이션 색상 계층: - 정적 항적: 회색 [180,180,180,80] / 연파랑 [100,140,200,60] - TripsLayer: 고채도 노랑 [255,200,60,220] / 고채도 파랑 [100,180,255,220] 패널 레이아웃: - 토글 패널: position: sticky left: 0 (항상 좌측 고정) - 모델 카드: 가로 스크롤 (maxWidth: calc(100vw - 340px)) - 다중 토글 유지, 화면 초과 시 스크롤 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
5002105d18
커밋
6789f82e3b
@ -203,7 +203,7 @@ const CorrelationPanel = ({
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: historyActive ? 100 : 20,
|
||||
left: 'calc(50% - 275px)',
|
||||
left: 10,
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
alignItems: 'flex-start',
|
||||
@ -212,6 +212,9 @@ const CorrelationPanel = ({
|
||||
fontSize: 10,
|
||||
color: '#e2e8f0',
|
||||
pointerEvents: 'auto',
|
||||
maxWidth: 'calc(100vw - 340px)',
|
||||
overflowX: 'auto',
|
||||
overflowY: 'visible',
|
||||
}}>
|
||||
{/* 고정: 토글 패널 */}
|
||||
<div style={{
|
||||
@ -219,6 +222,8 @@ const CorrelationPanel = ({
|
||||
border: '1px solid rgba(249,115,22,0.3)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 10px',
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
minWidth: 165,
|
||||
flexShrink: 0,
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
|
||||
@ -253,18 +258,13 @@ const CorrelationPanel = ({
|
||||
const gc = modelItems.filter(c => c.targetType !== 'VESSEL').length;
|
||||
return (
|
||||
<label key={m.name} style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, cursor: 'pointer', marginBottom: 3 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabledModels.has(m.name)}
|
||||
<input type="checkbox" checked={enabledModels.has(m.name)}
|
||||
onChange={() => onEnabledModelsChange(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(m.name)) next.delete(m.name);
|
||||
else next.add(m.name);
|
||||
if (next.has(m.name)) next.delete(m.name); else next.add(m.name);
|
||||
return next;
|
||||
})}
|
||||
style={{ accentColor: color, width: 11, height: 11 }}
|
||||
title={m.name}
|
||||
/>
|
||||
style={{ accentColor: color, width: 11, height: 11 }} title={m.name} />
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: color, flexShrink: 0 }} />
|
||||
<span style={{ color: '#e2e8f0', flex: 1 }}>{m.name}{m.isDefault ? '*' : ''}</span>
|
||||
<span style={{ color: '#64748b', fontSize: 8 }}>{vc}⛴{gc}◆</span>
|
||||
|
||||
@ -95,10 +95,9 @@ export function useGearReplayLayers(
|
||||
|
||||
const layers: Layer[] = [];
|
||||
|
||||
// ── Static layers (center trail + dots) ───────────────────────────────
|
||||
// ── 항상 표시: 센터 트레일 + 도트 ──────────────────────────────────
|
||||
|
||||
// Center trail segments (PathLayer) — showTrails 제어
|
||||
if (showTrails) {
|
||||
// Center trail segments (PathLayer) — 항상 ON
|
||||
for (let i = 0; i < centerTrailSegments.length; i++) {
|
||||
const seg = centerTrailSegments[i];
|
||||
if (seg.path.length < 2) continue;
|
||||
@ -112,10 +111,9 @@ export function useGearReplayLayers(
|
||||
widthMinPixels: 2,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Center dots (real data only) — showTrails 제어
|
||||
if (showTrails && centerDotsPositions.length > 0) {
|
||||
// Center dots (real data only) — 항상 ON
|
||||
if (centerDotsPositions.length > 0) {
|
||||
layers.push(new ScatterplotLayer({
|
||||
id: 'replay-center-dots',
|
||||
data: centerDotsPositions,
|
||||
@ -143,17 +141,54 @@ export function useGearReplayLayers(
|
||||
const members = interpolateMemberPositions(state.historyFrames, frameIdx, ct);
|
||||
const memberPts: [number, number][] = members.map(m => [m.lon, m.lat]);
|
||||
|
||||
// 1. Identity 모델: 멤버 트레일 + 폴리곤 + 마커 (enabledModels 체크)
|
||||
// ── "항적" 토글: 전체 24h 경로 PathLayer (정적 배경) ─────────────
|
||||
if (showTrails) {
|
||||
// 멤버 전체 항적 (identity)
|
||||
if (enabledModels.has('identity') && memberTripsData.length > 0) {
|
||||
for (const trip of memberTripsData) {
|
||||
if (trip.path.length < 2) continue;
|
||||
layers.push(new PathLayer({
|
||||
id: `replay-member-path-${trip.id}`,
|
||||
data: [{ path: trip.path }],
|
||||
getPath: (d: { path: [number, number][] }) => d.path,
|
||||
getColor: [180, 180, 180, 80], // 낮은 채도 — TripsLayer보다 연하게
|
||||
widthMinPixels: 1,
|
||||
}));
|
||||
}
|
||||
}
|
||||
// 연관 선박 전체 항적 (correlation)
|
||||
if (correlationTripsData.length > 0) {
|
||||
const activeMmsis = new Set<string>();
|
||||
for (const [mn, items] of correlationByModel) {
|
||||
if (!enabledModels.has(mn)) continue;
|
||||
for (const c of items as GearCorrelationItem[]) {
|
||||
if (enabledVessels.has(c.targetMmsi)) activeMmsis.add(c.targetMmsi);
|
||||
}
|
||||
}
|
||||
for (const trip of correlationTripsData) {
|
||||
if (!activeMmsis.has(trip.id) || trip.path.length < 2) continue;
|
||||
layers.push(new PathLayer({
|
||||
id: `replay-corr-path-${trip.id}`,
|
||||
data: [{ path: trip.path }],
|
||||
getPath: (d: { path: [number, number][] }) => d.path,
|
||||
getColor: [100, 140, 200, 60], // 연한 파랑
|
||||
widthMinPixels: 1,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Identity 모델: TripsLayer + 폴리곤 + 마커 (항상 ON)
|
||||
if (enabledModels.has('identity')) {
|
||||
// TripsLayer — member trails (GPU animated, showTrails 제어)
|
||||
if (showTrails && memberTripsData.length > 0) {
|
||||
// TripsLayer — member trails (GPU animated, 항상 ON, 고채도)
|
||||
if (memberTripsData.length > 0) {
|
||||
layers.push(new TripsLayer({
|
||||
id: 'replay-member-trails',
|
||||
data: memberTripsData,
|
||||
getPath: d => d.path,
|
||||
getTimestamps: d => d.timestamps,
|
||||
getColor: d => d.color,
|
||||
widthMinPixels: 1.5,
|
||||
getColor: [255, 200, 60, 220], // 고채도 노란색 (항적보다 밝게)
|
||||
widthMinPixels: 2,
|
||||
fadeTrail: true,
|
||||
trailLength: TRAIL_LENGTH_MS,
|
||||
currentTime: ct - st,
|
||||
@ -177,9 +212,8 @@ export function useGearReplayLayers(
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Correlation trails (GPU animated, showTrails + enabledModels 체크)
|
||||
if (showTrails && correlationTripsData.length > 0) {
|
||||
// 활성 모델에 속하는 선박의 트랙만 표시
|
||||
// 2. Correlation TripsLayer (GPU animated, 항상 ON, 고채도)
|
||||
if (correlationTripsData.length > 0) {
|
||||
const activeMmsis = new Set<string>();
|
||||
for (const [mn, items] of correlationByModel) {
|
||||
if (!enabledModels.has(mn)) continue;
|
||||
@ -194,8 +228,8 @@ export function useGearReplayLayers(
|
||||
data: enabledTrips,
|
||||
getPath: d => d.path,
|
||||
getTimestamps: d => d.timestamps,
|
||||
getColor: d => d.color,
|
||||
widthMinPixels: 2,
|
||||
getColor: [100, 180, 255, 220], // 고채도 파랑 (항적보다 밝게)
|
||||
widthMinPixels: 2.5,
|
||||
fadeTrail: true,
|
||||
trailLength: TRAIL_LENGTH_MS,
|
||||
currentTime: ct - st,
|
||||
@ -342,18 +376,19 @@ export function useGearReplayLayers(
|
||||
if (shouldLog) {
|
||||
const trackHit = corrPositions.filter(p => corrTrackMap.has(p.mmsi)).length;
|
||||
const liveHit = corrPositions.length - trackHit;
|
||||
const sampleTrip = memberTripsData[0];
|
||||
console.log('[GearReplay] renderFrame:', {
|
||||
historyFrames: state.historyFrames.length,
|
||||
correlationByModel: state.correlationByModel.size,
|
||||
modelNames: [...state.correlationByModel.keys()],
|
||||
memberTripsData: memberTripsData.length,
|
||||
corrTripsData: correlationTripsData.length,
|
||||
corrTrackMap: corrTrackMap.size,
|
||||
enabledModels: [...enabledModels],
|
||||
enabledVessels: enabledVessels.size,
|
||||
showTrails, showLabels,
|
||||
relTime: Math.round(relTime / 60000) + 'min',
|
||||
currentTime: Math.round((ct - st) / 60000) + 'min (rel)',
|
||||
members: members.length,
|
||||
corrPositions: corrPositions.length,
|
||||
posSource: `track:${trackHit} live:${liveHit}`,
|
||||
memberTrip0: sampleTrip ? { id: sampleTrip.id, pts: sampleTrip.path.length, tsRange: `${Math.round(sampleTrip.timestamps[0]/60000)}~${Math.round(sampleTrip.timestamps[sampleTrip.timestamps.length-1]/60000)}min` } : 'none',
|
||||
});
|
||||
// 모델별 상세
|
||||
for (const [mn, items] of state.correlationByModel) {
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user