import { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocalStorageSet } from '../../hooks/useLocalStorage'; // Aircraft category colors (matches AircraftLayer military fixed colors) const AC_CAT_COLORS: Record = { fighter: '#ff4444', military: '#ff6600', surveillance: '#ffcc00', tanker: '#00ccff', cargo: '#a78bfa', civilian: '#FFD700', unknown: '#7CFC00', }; // Altitude color legend (matches AircraftLayer gradient) const ALT_LEGEND: [string, string][] = [ ['Ground', '#555555'], ['< 2,000ft', '#00c000'], ['2,000ft', '#55EC55'], ['4,000ft', '#7CFC00'], ['6,000ft', '#BFFF00'], ['10,000ft', '#FFFF00'], ['20,000ft', '#FFD700'], ['30,000ft', '#FF8C00'], ['40,000ft', '#FF4500'], ['50,000ft+', '#BA55D3'], ]; // Military color legend const MIL_LEGEND: [string, string][] = [ ['Fighter', '#ff4444'], ['Military', '#ff6600'], ['ISR / Surveillance', '#ffcc00'], ['Tanker', '#00ccff'], ]; // Ship MT category color (matches ShipLayer MT_TYPE_COLORS) const MT_CAT_COLORS: Record = { cargo: 'var(--kcg-ship-cargo)', tanker: 'var(--kcg-ship-tanker)', passenger: 'var(--kcg-ship-passenger)', fishing: 'var(--kcg-ship-fishing)', fishing_gear: '#f97316', military: 'var(--kcg-ship-military)', tug_special: 'var(--kcg-ship-tug)', high_speed: 'var(--kcg-ship-highspeed)', pleasure: 'var(--kcg-ship-pleasure)', other: 'var(--kcg-ship-other)', unspecified: 'var(--kcg-ship-unknown)', unknown: 'var(--kcg-ship-unknown)', }; // Ship type color legend (MarineTraffic style) const SHIP_TYPE_LEGEND: [string, string][] = [ ['cargo', 'var(--kcg-ship-cargo)'], ['tanker', 'var(--kcg-ship-tanker)'], ['passenger', 'var(--kcg-ship-passenger)'], ['fishing', 'var(--kcg-ship-fishing)'], ['fishing_gear', '#f97316'], ['pleasure', 'var(--kcg-ship-pleasure)'], ['military', 'var(--kcg-ship-military)'], ['tug_special', 'var(--kcg-ship-tug)'], ['other', 'var(--kcg-ship-other)'], ['unspecified', 'var(--kcg-ship-unknown)'], ]; const AC_CATEGORIES = ['fighter', 'military', 'surveillance', 'tanker', 'cargo', 'civilian', 'unknown'] as const; const MT_CATEGORIES = ['cargo', 'tanker', 'passenger', 'fishing', 'fishing_gear', 'military', 'tug_special', 'high_speed', 'pleasure', 'other', 'unspecified'] as const; // Nationality categories for Korea tab const NAT_CATEGORIES = ['KR', 'CN', 'KP', 'JP', 'unclassified'] as const; // Fishing vessel nationality categories const FISHING_NAT_CATEGORIES = ['CN', 'KR', 'JP', 'other'] as const; const FISHING_NAT_LABELS: Record = { CN: 'πŸ‡¨πŸ‡³ 쀑ꡭ어선', KR: 'πŸ‡°πŸ‡· ν•œκ΅­μ–΄μ„ ', JP: 'πŸ‡―πŸ‡΅ 일본어선', other: '🏳️ 기타어선', }; const FISHING_NAT_COLORS: Record = { CN: '#ef4444', KR: '#3b82f6', JP: '#f472b6', other: '#6b7280', }; const NAT_LABELS: Record = { KR: 'πŸ‡°πŸ‡· ν•œκ΅­', CN: 'πŸ‡¨πŸ‡³ 쀑ꡭ', KP: 'πŸ‡°πŸ‡΅ λΆν•œ', JP: 'πŸ‡―πŸ‡΅ 일본', unclassified: '🏳️ λ―ΈλΆ„λ₯˜', }; const NAT_COLORS: Record = { KR: '#3b82f6', CN: '#ef4444', KP: '#f97316', JP: '#f472b6', unclassified: '#6b7280', }; interface ExtraLayer { key: string; label: string; color: string; count?: number; group?: string; } const GROUP_META: Record = { '항곡망': { label: '항곡망', color: '#22d3ee' }, 'ν•΄μ–‘μ•ˆμ „': { label: 'ν•΄μ–‘μ•ˆμ „', color: '#3b82f6' }, 'ꡭ가기관망': { label: 'ꡭ가기관망', color: '#f59e0b' }, 'μœ„ν—˜μ‹œμ„€': { label: 'μœ„ν—˜μ‹œμ„€', color: '#ef4444', superGroup: 'μœ„ν—˜/μ‚°μ—… 인프라' }, 'μ—λ„ˆμ§€/λ°œμ „μ‹œμ„€': { label: 'μ—λ„ˆμ§€/λ°œμ „μ‹œμ„€', color: '#a855f7', superGroup: 'μœ„ν—˜/μ‚°μ—… 인프라' }, '산업곡정/μ œμ‘°μ‹œμ„€': { label: '산업곡정/μ œμ‘°μ‹œμ„€', color: '#0ea5e9', superGroup: 'μœ„ν—˜/μ‚°μ—… 인프라' }, }; const SUPER_GROUP_META: Record = { 'μœ„ν—˜/μ‚°μ—… 인프라': { label: 'μœ„ν—˜/μ‚°μ—… 인프라', color: '#f97316' }, }; interface OverseasItem { key: string; label: string; color: string; count?: number; children?: OverseasItem[]; } function countOverseasActiveLeaves(items: OverseasItem[], layers: Record): number { let count = 0; for (const item of items) { if (item.children?.length) { count += countOverseasActiveLeaves(item.children, layers); } else if (layers[item.key]) { count += (item.count ?? 1); } } return count; } interface LayerPanelProps { layers: Record; onToggle: (key: string) => void; aircraftByCategory: Record; aircraftTotal: number; shipsByMtCategory: Record; shipTotal: number; satelliteCount: number; extraLayers?: ExtraLayer[]; overseasItems?: OverseasItem[]; hiddenAcCategories: Set; hiddenShipCategories: Set; onAcCategoryToggle: (cat: string) => void; onShipCategoryToggle: (cat: string) => void; shipsByNationality?: Record; hiddenNationalities?: Set; onNationalityToggle?: (nat: string) => void; fishingByNationality?: Record; hiddenFishingNats?: Set; onFishingNatToggle?: (nat: string) => void; } export function LayerPanel({ layers, onToggle, aircraftByCategory, aircraftTotal, shipsByMtCategory, shipTotal, satelliteCount, extraLayers, overseasItems, hiddenAcCategories, hiddenShipCategories, onAcCategoryToggle, onShipCategoryToggle, shipsByNationality, hiddenNationalities, onNationalityToggle, fishingByNationality, hiddenFishingNats, onFishingNatToggle, }: LayerPanelProps) { const { t } = useTranslation(['common', 'ships']); const [expanded, setExpanded] = useLocalStorageSet('layerPanelExpanded', new Set(['ships'])); const [legendOpen, setLegendOpen] = useState>(new Set()); const toggleExpand = useCallback((key: string) => { setExpanded(prev => { const next = new Set(prev); if (next.has(key)) { next.delete(key); } else { next.add(key); } return next; }); }, [setExpanded]); const toggleLegend = useCallback((key: string) => { setLegendOpen(prev => { const next = new Set(prev); if (next.has(key)) { next.delete(key); } else { next.add(key); } return next; }); }, []); const _militaryCount = Object.entries(aircraftByCategory) .filter(([cat]) => cat !== 'civilian' && cat !== 'unknown') .reduce((sum, [, c]) => sum + c, 0); void _militaryCount; // μ΄λž€ νƒ­μ—μ„œ μ‚¬μš© κ°€λŠ₯ β€” ν•΄μ™Έμ‹œμ„€ 뢄리 ν›„ λ―Έμ‚¬μš© return (

LAYERS

{/* ═══ μ„ λ°• (μ΅œμƒμœ„) ═══ */} {/* Ships tree */} onToggle('ships')} onExpand={() => toggleExpand('ships')} /> {layers.ships && expanded.has('ships') && (
{MT_CATEGORIES.map(cat => { const count = shipsByMtCategory[cat] || 0; if (count === 0) return null; // 어선은 ꡭ적별 ν•˜μœ„ λΆ„λ₯˜ ν‘œμ‹œ if (cat === 'fishing' && fishingByNationality && hiddenFishingNats && onFishingNatToggle) { const isFishingExpanded = expanded.has('fishing-sub'); return (
{ e.stopPropagation(); toggleExpand('fishing-sub'); }} > {isFishingExpanded ? 'β–Ό' : 'β–Ά'}
{isFishingExpanded && !hiddenShipCategories.has('fishing') && (
{FISHING_NAT_CATEGORIES.map(nat => { const fCount = fishingByNationality[nat] || 0; if (fCount === 0) return null; return (
)}
); } return (
)} {/* Nationality tree (Korea tab only) */} {shipsByNationality && hiddenNationalities && onNationalityToggle && ( <> a + b, 0)} color="#8b5cf6" active expandable isExpanded={expanded.has('nationality')} onToggle={() => toggleExpand('nationality')} onExpand={() => toggleExpand('nationality')} /> {expanded.has('nationality') && (
{NAT_CATEGORIES.map(nat => { const count = shipsByNationality[nat] || 0; if (count === 0) return null; return (
)} )} {/* ═══ 항곡망 κ·Έλ£Ή ═══ */} toggleExpand('group-항곡망')} onExpand={() => toggleExpand('group-항곡망')} /> {expanded.has('group-항곡망') && (
{/* Aircraft tree */} onToggle('aircraft')} onExpand={() => toggleExpand('aircraft')} /> {layers.aircraft && expanded.has('aircraft') && (
{AC_CATEGORIES.map(cat => { const count = aircraftByCategory[cat] || 0; if (count === 0) return null; return (
)} {/* Satellites */} onToggle('satellites')} />
)} {/* Extra layers β€” grouped */} {extraLayers && (() => { const grouped: Record = {}; const ungrouped: ExtraLayer[] = []; for (const el of extraLayers) { if (el.group) { if (!grouped[el.group]) grouped[el.group] = []; grouped[el.group].push(el); } else { ungrouped.push(el); } } // 수퍼그룹 λ³„λ‘œ κ·Έλ£Ή λΆ„λ₯˜ const superGrouped: Record = {}; // superGroup β†’ groupNames[] const noSuperGroup: string[] = []; for (const groupName of Object.keys(grouped)) { const sg = GROUP_META[groupName]?.superGroup; if (sg) { if (!superGrouped[sg]) superGrouped[sg] = []; superGrouped[sg].push(groupName); } else { noSuperGroup.push(groupName); } } const renderGroup = (groupName: string, indent = false) => { const meta = GROUP_META[groupName] || { label: groupName, color: '#888' }; const isGroupExpanded = expanded.has(`group-${groupName}`); const items = grouped[groupName] || []; return (
toggleExpand(`group-${groupName}`)} onExpand={() => toggleExpand(`group-${groupName}`)} /> {isGroupExpanded && (
{items.map(el => ( onToggle(el.key)} /> ))}
)}
); }; return ( <> {/* 수퍼그룹 μ—†λŠ” κ·Έλ£Ήλ“€ (ν•­κ³΅λ§Β·ν•΄μ–‘μ•ˆμ „Β·κ΅­κ°€κΈ°κ΄€λ§) */} {noSuperGroup.map(g => renderGroup(g))} {/* 수퍼그룹으둜 묢인 κ·Έλ£Ήλ“€ */} {Object.entries(superGrouped).map(([sgName, groupNames]) => { const sgMeta = SUPER_GROUP_META[sgName] || { label: sgName, color: '#f97316' }; const isSgExpanded = expanded.has(`supergroup-${sgName}`); return (
toggleExpand(`supergroup-${sgName}`)} onExpand={() => toggleExpand(`supergroup-${sgName}`)} /> {isSgExpanded && (
{groupNames.map(g => renderGroup(g, true))}
)}
); })} {/* κ·Έλ£Ή μ—†λŠ” κ°œλ³„ λ ˆμ΄μ–΄ */} {ungrouped.map(el => ( onToggle(el.key)} /> ))} ); })()}
{/* ν•΄μ™Έμ‹œμ„€ β€” μ ‘κΈ°/펼치기 μ „μš© (토글은 ν•˜μœ„ ν•­λͺ©μ—μ„œ κ°œλ³„ μ œμ–΄) */} toggleExpand('overseas-section')} onExpand={() => toggleExpand('overseas-section')} /> {expanded.has('overseas-section') && overseasItems && overseasItems.length > 0 && (
{overseasItems.map(item => ( ))}
)}
); } /* ── Overseas 3-level tree node ────────────────────── */ function OverseasTreeNode({ item, depth, layers, expanded, onToggle, onToggleAll, toggleExpand }: { item: OverseasItem; depth: number; layers: Record; expanded: Set; onToggle: (key: string) => void; onToggleAll: (key: string) => void; toggleExpand: (key: string) => void; }) { const hasChildren = item.children && item.children.length > 0; const isExpanded = expanded.has(item.key); const isActive = hasChildren ? item.children!.some(c => c.children?.length ? c.children.some(gc => layers[gc.key]) : layers[c.key]) : layers[item.key]; const leafCount = hasChildren ? countOverseasActiveLeaves([item], layers) : (item.count ?? 0); const handleToggle = () => { if (hasChildren) { // λΆ€λͺ¨ ν† κΈ€ β†’ λͺ¨λ“  ν•˜μœ„ 리프 on/off const allLeaves: string[] = []; const collectLeaves = (node: OverseasItem) => { if (node.children?.length) node.children.forEach(collectLeaves); else allLeaves.push(node.key); }; collectLeaves(item); const allOn = allLeaves.every(k => layers[k]); for (const k of allLeaves) { if (allOn || !layers[k]) onToggleAll(k); } } else { onToggle(item.key); } }; return (
toggleExpand(item.key)} /> {hasChildren && isExpanded && (
{item.children!.map(child => ( ))}
)}
); } /* ── Sub-components ─────────────────────────────────── */ function LayerTreeItem({ layerKey, label, color, active, expandable, isExpanded, count, onToggle, onExpand, }: { layerKey: string; label: string; color: string; active: boolean; expandable?: boolean; isExpanded?: boolean; count?: number; onToggle: () => void; onExpand?: () => void; }) { return (
{expandable ? ( { e.stopPropagation(); onExpand?.(); }} > {'\u25B6'} ) : ( )}
); } function CategoryToggle({ label, color, count, hidden, onClick, }: { label: string; color: string; count: number; hidden: boolean; onClick: () => void; }) { return (
{label} {count}
); }