Merge pull request 'fix: 이름 기반 레이어 항상 ON + 최상위 z-index' (#208) from feature/identity-layer-fix into develop
This commit is contained in:
커밋
b14df41da7
@ -21,6 +21,7 @@
|
|||||||
- enabledVessels 토글 시 폴리곤 + 중심경로 동시 재계산
|
- enabledVessels 토글 시 폴리곤 + 중심경로 동시 재계산
|
||||||
|
|
||||||
### 수정
|
### 수정
|
||||||
|
- 이름 기반 레이어 항상 ON 고정 + 최상위 z-index (다른 모델에 가려지지 않음)
|
||||||
- Prediction API DB 접속 context manager 누락
|
- Prediction API DB 접속 context manager 누락
|
||||||
- Prediction proxy rewrite 경로 불일치 (/api/prediction → /api)
|
- Prediction proxy rewrite 경로 불일치 (/api/prediction → /api)
|
||||||
- nginx 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 }}>
|
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, cursor: 'pointer', marginBottom: 3 }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={enabledModels.has('identity')}
|
checked={true}
|
||||||
onChange={() => onEnabledModelsChange(prev => {
|
disabled
|
||||||
const next = new Set(prev);
|
style={{ accentColor: '#f97316', width: 11, height: 11, opacity: 0.6 }}
|
||||||
if (next.has('identity')) next.delete('identity');
|
title="이름 기반 (항상 ON)"
|
||||||
else next.add('identity');
|
|
||||||
return next;
|
|
||||||
})}
|
|
||||||
style={{ accentColor: '#f97316', width: 11, height: 11 }}
|
|
||||||
title="이름 기반"
|
|
||||||
/>
|
/>
|
||||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#f97316', flexShrink: 0 }} />
|
<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>
|
<span style={{ color: '#64748b', marginLeft: 'auto' }}>{memberCount}</span>
|
||||||
</label>
|
</label>
|
||||||
{correlationLoading && <div style={{ fontSize: 8, color: '#64748b' }}>로딩...</div>}
|
{correlationLoading && <div style={{ fontSize: 8, color: '#64748b' }}>로딩...</div>}
|
||||||
|
|||||||
@ -144,8 +144,8 @@ export function useGearReplayLayers(
|
|||||||
|
|
||||||
// ── "항적" 토글: 전체 24h 경로 PathLayer (정적 배경) ─────────────
|
// ── "항적" 토글: 전체 24h 경로 PathLayer (정적 배경) ─────────────
|
||||||
if (showTrails) {
|
if (showTrails) {
|
||||||
// 멤버 전체 항적 (identity)
|
// 멤버 전체 항적 (identity — 항상 ON)
|
||||||
if (enabledModels.has('identity') && memberTripsData.length > 0) {
|
if (memberTripsData.length > 0) {
|
||||||
for (const trip of memberTripsData) {
|
for (const trip of memberTripsData) {
|
||||||
if (trip.path.length < 2) continue;
|
if (trip.path.length < 2) continue;
|
||||||
layers.push(new PathLayer({
|
layers.push(new PathLayer({
|
||||||
@ -179,41 +179,7 @@ export function useGearReplayLayers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Identity 모델: TripsLayer + 폴리곤 + 마커 (항상 ON)
|
// 1. Correlation TripsLayer (GPU animated, 항상 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, 고채도)
|
|
||||||
if (correlationTripsData.length > 0) {
|
if (correlationTripsData.length > 0) {
|
||||||
const activeMmsis = new Set<string>();
|
const activeMmsis = new Set<string>();
|
||||||
for (const [mn, items] of correlationByModel) {
|
for (const [mn, items] of correlationByModel) {
|
||||||
@ -238,22 +204,10 @@ export function useGearReplayLayers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Current center point
|
// (identity 레이어는 최하단 — 최상위 z-index로 이동됨)
|
||||||
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,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 5. Member position markers (IconLayer, identity 모델 활성 시만)
|
// 3. Member position markers (IconLayer, identity — 항상 ON, placeholder)
|
||||||
if (members.length > 0 && enabledModels.has('identity')) {
|
if (members.length > 0) {
|
||||||
layers.push(new IconLayer<MemberPosition>({
|
layers.push(new IconLayer<MemberPosition>({
|
||||||
id: 'replay-members',
|
id: 'replay-members',
|
||||||
data: members,
|
data: members,
|
||||||
@ -501,7 +455,7 @@ export function useGearReplayLayers(
|
|||||||
}
|
}
|
||||||
if (extraPts.length === 0) continue;
|
if (extraPts.length === 0) continue;
|
||||||
|
|
||||||
const basePts = enabledModels.has('identity') ? memberPts : [];
|
const basePts = memberPts; // identity 항상 ON
|
||||||
const opPolygon = buildInterpPolygon([...basePts, ...extraPts]);
|
const opPolygon = buildInterpPolygon([...basePts, ...extraPts]);
|
||||||
if (!opPolygon) continue;
|
if (!opPolygon) continue;
|
||||||
|
|
||||||
@ -579,12 +533,11 @@ export function useGearReplayLayers(
|
|||||||
const badgeTargets = new Map<string, { lon: number; lat: number; models: Set<string> }>();
|
const badgeTargets = new Map<string, { lon: number; lat: number; models: Set<string> }>();
|
||||||
|
|
||||||
// Identity model: group members
|
// Identity model: group members
|
||||||
if (enabledModels.has('identity')) {
|
// Identity — 항상 ON
|
||||||
for (const m of members) {
|
for (const m of members) {
|
||||||
const e = badgeTargets.get(m.mmsi) ?? { lon: m.lon, lat: m.lat, models: new Set<string>() };
|
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');
|
e.lon = m.lon; e.lat = m.lat; e.models.add('identity');
|
||||||
badgeTargets.set(m.mmsi, e);
|
badgeTargets.set(m.mmsi, e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Correlation models
|
// Correlation models
|
||||||
@ -627,10 +580,49 @@ export function useGearReplayLayers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 디버그: 연관 선박 렌더링 상태
|
// ══ Identity 레이어 (최상위 z-index — 항상 ON, 다른 모델 위에 표시) ══
|
||||||
if (corrPositions.length > 0 && !debugLoggedRef.current) {
|
// 폴리곤
|
||||||
console.log('[GearReplay] corrPositions:', corrPositions.length, 'operationalPolygons:', layers.filter(l => l.id?.toString().startsWith('replay-op-')).length);
|
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;
|
replayLayerRef.current = layers;
|
||||||
requestRender();
|
requestRender();
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user