fix: 이름 기반 레이어 항상 ON + 최상위 z-index #208
@ -21,6 +21,7 @@
|
||||
- enabledVessels 토글 시 폴리곤 + 중심경로 동시 재계산
|
||||
|
||||
### 수정
|
||||
- 이름 기반 레이어 항상 ON 고정 + 최상위 z-index (다른 모델에 가려지지 않음)
|
||||
- Prediction API DB 접속 context manager 누락
|
||||
- Prediction proxy rewrite 경로 불일치 (/api/prediction → /api)
|
||||
- nginx prediction API 라우팅 추가
|
||||
|
||||
@ -236,18 +236,13 @@ const CorrelationPanel = ({
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, cursor: 'pointer', marginBottom: 3 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabledModels.has('identity')}
|
||||
onChange={() => onEnabledModelsChange(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has('identity')) next.delete('identity');
|
||||
else next.add('identity');
|
||||
return next;
|
||||
})}
|
||||
style={{ accentColor: '#f97316', width: 11, height: 11 }}
|
||||
title="이름 기반"
|
||||
checked={true}
|
||||
disabled
|
||||
style={{ accentColor: '#f97316', width: 11, height: 11, opacity: 0.6 }}
|
||||
title="이름 기반 (항상 ON)"
|
||||
/>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#f97316', flexShrink: 0 }} />
|
||||
<span style={{ color: '#e2e8f0' }}>이름 기반</span>
|
||||
<span style={{ color: '#94a3b8' }}>이름 기반 (고정)</span>
|
||||
<span style={{ color: '#64748b', marginLeft: 'auto' }}>{memberCount}</span>
|
||||
</label>
|
||||
{correlationLoading && <div style={{ fontSize: 8, color: '#64748b' }}>로딩...</div>}
|
||||
|
||||
@ -144,8 +144,8 @@ export function useGearReplayLayers(
|
||||
|
||||
// ── "항적" 토글: 전체 24h 경로 PathLayer (정적 배경) ─────────────
|
||||
if (showTrails) {
|
||||
// 멤버 전체 항적 (identity)
|
||||
if (enabledModels.has('identity') && memberTripsData.length > 0) {
|
||||
// 멤버 전체 항적 (identity — 항상 ON)
|
||||
if (memberTripsData.length > 0) {
|
||||
for (const trip of memberTripsData) {
|
||||
if (trip.path.length < 2) continue;
|
||||
layers.push(new PathLayer({
|
||||
@ -179,41 +179,7 @@ export function useGearReplayLayers(
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Identity 모델: TripsLayer + 폴리곤 + 마커 (항상 ON)
|
||||
if (enabledModels.has('identity')) {
|
||||
// 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: [255, 200, 60, 220], // 고채도 노란색 (항적보다 밝게)
|
||||
widthMinPixels: 2,
|
||||
fadeTrail: true,
|
||||
trailLength: TRAIL_LENGTH_MS,
|
||||
currentTime: ct - st,
|
||||
}));
|
||||
}
|
||||
|
||||
// Current animated polygon (convex hull of members)
|
||||
const polygon = buildInterpPolygon(memberPts);
|
||||
if (polygon) {
|
||||
layers.push(new PolygonLayer({
|
||||
id: 'replay-polygon',
|
||||
data: [{ polygon: polygon.coordinates }],
|
||||
getPolygon: (d: { polygon: number[][][] }) => d.polygon,
|
||||
getFillColor: isStale ? [148, 163, 184, 30] : [251, 191, 36, 40],
|
||||
getLineColor: isStale ? [148, 163, 184, 100] : [251, 191, 36, 180],
|
||||
getLineWidth: isStale ? 1 : 2,
|
||||
lineWidthMinPixels: 1,
|
||||
filled: true,
|
||||
stroked: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Correlation TripsLayer (GPU animated, 항상 ON, 고채도)
|
||||
// 1. Correlation TripsLayer (GPU animated, 항상 ON, 고채도)
|
||||
if (correlationTripsData.length > 0) {
|
||||
const activeMmsis = new Set<string>();
|
||||
for (const [mn, items] of correlationByModel) {
|
||||
@ -238,22 +204,10 @@ export function useGearReplayLayers(
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Current center point
|
||||
layers.push(new ScatterplotLayer({
|
||||
id: 'replay-center',
|
||||
data: [{ position: [frame.centerLon, frame.centerLat] as [number, number] }],
|
||||
getPosition: (d: { position: [number, number] }) => d.position,
|
||||
getFillColor: isStale ? [249, 115, 22, 255] : [239, 68, 68, 255],
|
||||
getRadius: 200,
|
||||
radiusUnits: 'meters',
|
||||
radiusMinPixels: 7,
|
||||
stroked: true,
|
||||
getLineColor: [255, 255, 255, 255],
|
||||
lineWidthMinPixels: 2,
|
||||
}));
|
||||
// (identity 레이어는 최하단 — 최상위 z-index로 이동됨)
|
||||
|
||||
// 5. Member position markers (IconLayer, identity 모델 활성 시만)
|
||||
if (members.length > 0 && enabledModels.has('identity')) {
|
||||
// 3. Member position markers (IconLayer, identity — 항상 ON, placeholder)
|
||||
if (members.length > 0) {
|
||||
layers.push(new IconLayer<MemberPosition>({
|
||||
id: 'replay-members',
|
||||
data: members,
|
||||
@ -501,7 +455,7 @@ export function useGearReplayLayers(
|
||||
}
|
||||
if (extraPts.length === 0) continue;
|
||||
|
||||
const basePts = enabledModels.has('identity') ? memberPts : [];
|
||||
const basePts = memberPts; // identity 항상 ON
|
||||
const opPolygon = buildInterpPolygon([...basePts, ...extraPts]);
|
||||
if (!opPolygon) continue;
|
||||
|
||||
@ -579,12 +533,11 @@ export function useGearReplayLayers(
|
||||
const badgeTargets = new Map<string, { lon: number; lat: number; models: Set<string> }>();
|
||||
|
||||
// Identity model: group members
|
||||
if (enabledModels.has('identity')) {
|
||||
for (const m of members) {
|
||||
const e = badgeTargets.get(m.mmsi) ?? { lon: m.lon, lat: m.lat, models: new Set<string>() };
|
||||
e.lon = m.lon; e.lat = m.lat; e.models.add('identity');
|
||||
badgeTargets.set(m.mmsi, e);
|
||||
}
|
||||
// Identity — 항상 ON
|
||||
for (const m of members) {
|
||||
const e = badgeTargets.get(m.mmsi) ?? { lon: m.lon, lat: m.lat, models: new Set<string>() };
|
||||
e.lon = m.lon; e.lat = m.lat; e.models.add('identity');
|
||||
badgeTargets.set(m.mmsi, e);
|
||||
}
|
||||
|
||||
// Correlation models
|
||||
@ -627,10 +580,49 @@ export function useGearReplayLayers(
|
||||
}
|
||||
}
|
||||
|
||||
// 디버그: 연관 선박 렌더링 상태
|
||||
if (corrPositions.length > 0 && !debugLoggedRef.current) {
|
||||
console.log('[GearReplay] corrPositions:', corrPositions.length, 'operationalPolygons:', layers.filter(l => l.id?.toString().startsWith('replay-op-')).length);
|
||||
// ══ Identity 레이어 (최상위 z-index — 항상 ON, 다른 모델 위에 표시) ══
|
||||
// 폴리곤
|
||||
const identityPolygon = buildInterpPolygon(memberPts);
|
||||
if (identityPolygon) {
|
||||
layers.push(new PolygonLayer({
|
||||
id: 'replay-identity-polygon',
|
||||
data: [{ polygon: identityPolygon.coordinates }],
|
||||
getPolygon: (d: { polygon: number[][][] }) => d.polygon,
|
||||
getFillColor: isStale ? [148, 163, 184, 30] : [251, 191, 36, 40],
|
||||
getLineColor: isStale ? [148, 163, 184, 100] : [251, 191, 36, 180],
|
||||
getLineWidth: isStale ? 1 : 2,
|
||||
lineWidthMinPixels: 1,
|
||||
filled: true,
|
||||
stroked: true,
|
||||
}));
|
||||
}
|
||||
// TripsLayer (멤버 트레일)
|
||||
if (memberTripsData.length > 0) {
|
||||
layers.push(new TripsLayer({
|
||||
id: 'replay-identity-trails',
|
||||
data: memberTripsData,
|
||||
getPath: d => d.path,
|
||||
getTimestamps: d => d.timestamps,
|
||||
getColor: [255, 200, 60, 220],
|
||||
widthMinPixels: 2,
|
||||
fadeTrail: true,
|
||||
trailLength: TRAIL_LENGTH_MS,
|
||||
currentTime: ct - st,
|
||||
}));
|
||||
}
|
||||
// 센터 포인트
|
||||
layers.push(new ScatterplotLayer({
|
||||
id: 'replay-identity-center',
|
||||
data: [{ position: [frame.centerLon, frame.centerLat] as [number, number] }],
|
||||
getPosition: (d: { position: [number, number] }) => d.position,
|
||||
getFillColor: isStale ? [249, 115, 22, 255] : [239, 68, 68, 255],
|
||||
getRadius: 200,
|
||||
radiusUnits: 'meters',
|
||||
radiusMinPixels: 7,
|
||||
stroked: true,
|
||||
getLineColor: [255, 255, 255, 255],
|
||||
lineWidthMinPixels: 2,
|
||||
}));
|
||||
|
||||
replayLayerRef.current = layers;
|
||||
requestRender();
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user