feat: LayerPanel 공통 트리 구조 + SVG 아이콘 전수 전환
- LayerTreeNode 공통 인터페이스 + LayerTreeRenderer 재귀 컴포넌트 - 한국/이란 양쪽 트리 데이터 정의 + batchToggle 캐스케이드 - 위험시설/해외시설 emoji→SVG IconLayer 전환 (12 SVG 함수, 3 IconLayer) - 부모 토글→하위 전체 ON/OFF, 카운트 합산 동기화 - 대시보드 탭 localStorage 영속화
This commit is contained in:
부모
13bdebb924
커밋
dc8a30a58b
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useReplay } from './hooks/useReplay';
|
import { useReplay } from './hooks/useReplay';
|
||||||
import { useMonitor } from './hooks/useMonitor';
|
import { useMonitor } from './hooks/useMonitor';
|
||||||
|
import { useLocalStorage } from './hooks/useLocalStorage';
|
||||||
import type { AppMode } from './types';
|
import type { AppMode } from './types';
|
||||||
import { useTheme } from './hooks/useTheme';
|
import { useTheme } from './hooks/useTheme';
|
||||||
import { useAuth } from './hooks/useAuth';
|
import { useAuth } from './hooks/useAuth';
|
||||||
@ -40,7 +41,7 @@ interface AuthenticatedAppProps {
|
|||||||
|
|
||||||
function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
|
function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
|
||||||
const [appMode, setAppMode] = useState<AppMode>('live');
|
const [appMode, setAppMode] = useState<AppMode>('live');
|
||||||
const [dashboardTab, setDashboardTab] = useState<'iran' | 'korea'>('iran');
|
const [dashboardTab, setDashboardTab] = useLocalStorage<'iran' | 'korea'>('dashboardTab', 'iran');
|
||||||
const [showCollectorMonitor, setShowCollectorMonitor] = useState(false);
|
const [showCollectorMonitor, setShowCollectorMonitor] = useState(false);
|
||||||
const [timeZone, setTimeZone] = useState<'KST' | 'UTC'>('KST');
|
const [timeZone, setTimeZone] = useState<'KST' | 'UTC'>('KST');
|
||||||
|
|
||||||
|
|||||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { IRAN_OIL_COUNT } from './createIranOilLayers';
|
import { IRAN_OIL_COUNT } from './createIranOilLayers';
|
||||||
import { IRAN_AIRPORT_COUNT } from './createIranAirportLayers';
|
import { IRAN_AIRPORT_COUNT } from './createIranAirportLayers';
|
||||||
@ -10,7 +10,7 @@ import { GlobeMap } from './GlobeMap';
|
|||||||
import { SatelliteMap } from './SatelliteMap';
|
import { SatelliteMap } from './SatelliteMap';
|
||||||
import { SensorChart } from '../common/SensorChart';
|
import { SensorChart } from '../common/SensorChart';
|
||||||
import { EventLog } from '../common/EventLog';
|
import { EventLog } from '../common/EventLog';
|
||||||
import { LayerPanel } from '../common/LayerPanel';
|
import { LayerPanel, type LayerTreeNode } from '../common/LayerPanel';
|
||||||
import { LiveControls } from '../common/LiveControls';
|
import { LiveControls } from '../common/LiveControls';
|
||||||
import { ReplayControls } from '../common/ReplayControls';
|
import { ReplayControls } from '../common/ReplayControls';
|
||||||
import { TimelineSlider } from '../common/TimelineSlider';
|
import { TimelineSlider } from '../common/TimelineSlider';
|
||||||
@ -113,11 +113,51 @@ const IranDashboard = ({
|
|||||||
setLayers(prev => ({ ...prev, [key]: !prev[key] }));
|
setLayers(prev => ({ ...prev, [key]: !prev[key] }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const batchToggleLayer = useCallback((keys: string[], value: boolean) => {
|
||||||
|
setLayers(prev => {
|
||||||
|
const next = { ...prev } as Record<string, boolean>;
|
||||||
|
for (const k of keys) next[k] = value;
|
||||||
|
return next as LayerVisibility;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleEventFlyTo = useCallback((event: GeoEvent) => {
|
const handleEventFlyTo = useCallback((event: GeoEvent) => {
|
||||||
setFlyToTarget({ lat: event.lat, lng: event.lng, zoom: 8 });
|
setFlyToTarget({ lat: event.lat, lng: event.lng, zoom: 8 });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const meCountByCountry = (ck: string) => ME_ENERGY_HAZARD_FACILITIES.filter(f => f.countryKey === ck).length;
|
const meCountByCountry = useCallback((ck: string) => ME_ENERGY_HAZARD_FACILITIES.filter(f => f.countryKey === ck).length, []);
|
||||||
|
|
||||||
|
const layerTree = useMemo((): LayerTreeNode[] => [
|
||||||
|
{ key: 'ships', label: t('layers.ships'), color: '#fb923c', count: iranData.ships.length, specialRenderer: 'shipCategories' },
|
||||||
|
{
|
||||||
|
key: 'aviation', label: '항공망', color: '#22d3ee',
|
||||||
|
children: [
|
||||||
|
{ key: 'aircraft', label: t('layers.aircraft'), color: '#22d3ee', count: iranData.aircraft.length, specialRenderer: 'aircraftCategories' },
|
||||||
|
{ key: 'satellites', label: t('layers.satellites'), color: '#ef4444', count: iranData.satPositions.length },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'events', label: t('layers.events'), color: '#a855f7' },
|
||||||
|
{ key: 'koreanShips', label: `\u{1F1F0}\u{1F1F7} ${t('layers.koreanShips')}`, color: '#00e5ff', count: iranData.koreanShips.length },
|
||||||
|
{ key: 'airports', label: t('layers.airports'), color: '#f59e0b', count: IRAN_AIRPORT_COUNT },
|
||||||
|
{ key: 'oilFacilities', label: t('layers.oilFacilities'), color: '#d97706', count: IRAN_OIL_COUNT },
|
||||||
|
{ key: 'meFacilities', label: '주요시설/군사', color: '#ef4444', count: ME_FACILITY_COUNT },
|
||||||
|
{ key: 'sensorCharts', label: t('layers.sensorCharts'), color: '#22c55e' },
|
||||||
|
{
|
||||||
|
key: 'overseas', label: '해외시설', color: '#f97316',
|
||||||
|
children: [
|
||||||
|
{ key: 'overseasUS', label: '🇺🇸 미국', color: '#3b82f6', count: meCountByCountry('us') },
|
||||||
|
{ key: 'overseasIsrael', label: '🇮🇱 이스라엘', color: '#dc2626', count: meCountByCountry('il') },
|
||||||
|
{ key: 'overseasIran', label: '🇮🇷 이란', color: '#22c55e', count: meCountByCountry('ir') },
|
||||||
|
{ key: 'overseasUAE', label: '🇦🇪 UAE', color: '#f59e0b', count: meCountByCountry('ae') },
|
||||||
|
{ key: 'overseasSaudi', label: '🇸🇦 사우디아라비아', color: '#84cc16', count: meCountByCountry('sa') },
|
||||||
|
{ key: 'overseasOman', label: '🇴🇲 오만', color: '#e11d48', count: meCountByCountry('om') },
|
||||||
|
{ key: 'overseasQatar', label: '🇶🇦 카타르', color: '#8b5cf6', count: meCountByCountry('qa') },
|
||||||
|
{ key: 'overseasKuwait', label: '🇰🇼 쿠웨이트', color: '#f97316', count: meCountByCountry('kw') },
|
||||||
|
{ key: 'overseasIraq', label: '🇮🇶 이라크', color: '#65a30d', count: meCountByCountry('iq') },
|
||||||
|
{ key: 'overseasBahrain', label: '🇧🇭 바레인', color: '#e11d48', count: meCountByCountry('bh') },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
], [iranData, t, meCountByCountry]);
|
||||||
|
|
||||||
// 헤더 슬롯 Portal — 이란 모드 토글 + 맵 모드 + 카운트
|
// 헤더 슬롯 Portal — 이란 모드 토글 + 맵 모드 + 카운트
|
||||||
const headerSlot = document.getElementById('dashboard-header-slot');
|
const headerSlot = document.getElementById('dashboard-header-slot');
|
||||||
@ -214,31 +254,13 @@ const IranDashboard = ({
|
|||||||
<LayerPanel
|
<LayerPanel
|
||||||
layers={layers as unknown as Record<string, boolean>}
|
layers={layers as unknown as Record<string, boolean>}
|
||||||
onToggle={toggleLayer as (key: string) => void}
|
onToggle={toggleLayer as (key: string) => void}
|
||||||
|
onBatchToggle={batchToggleLayer}
|
||||||
|
tree={layerTree}
|
||||||
aircraftByCategory={iranData.aircraftByCategory}
|
aircraftByCategory={iranData.aircraftByCategory}
|
||||||
aircraftTotal={iranData.aircraft.length}
|
aircraftTotal={iranData.aircraft.length}
|
||||||
shipsByMtCategory={iranData.shipsByCategory}
|
shipsByMtCategory={iranData.shipsByCategory}
|
||||||
shipTotal={iranData.ships.length}
|
shipTotal={iranData.ships.length}
|
||||||
satelliteCount={iranData.satPositions.length}
|
satelliteCount={iranData.satPositions.length}
|
||||||
extraLayers={[
|
|
||||||
{ key: 'events', label: t('layers.events'), color: '#a855f7' },
|
|
||||||
{ key: 'koreanShips', label: `\u{1F1F0}\u{1F1F7} ${t('layers.koreanShips')}`, color: '#00e5ff', count: iranData.koreanShips.length },
|
|
||||||
{ key: 'airports', label: t('layers.airports'), color: '#f59e0b', count: IRAN_AIRPORT_COUNT },
|
|
||||||
{ key: 'oilFacilities', label: t('layers.oilFacilities'), color: '#d97706', count: IRAN_OIL_COUNT },
|
|
||||||
{ key: 'meFacilities', label: '주요시설/군사', color: '#ef4444', count: ME_FACILITY_COUNT },
|
|
||||||
{ key: 'sensorCharts', label: t('layers.sensorCharts'), color: '#22c55e' },
|
|
||||||
]}
|
|
||||||
overseasItems={[
|
|
||||||
{ key: 'overseasUS', label: '🇺🇸 미국', color: '#3b82f6', count: meCountByCountry('us') },
|
|
||||||
{ key: 'overseasIsrael', label: '🇮🇱 이스라엘', color: '#dc2626', count: meCountByCountry('il') },
|
|
||||||
{ key: 'overseasIran', label: '🇮🇷 이란', color: '#22c55e', count: meCountByCountry('ir') },
|
|
||||||
{ key: 'overseasUAE', label: '🇦🇪 UAE', color: '#f59e0b', count: meCountByCountry('ae') },
|
|
||||||
{ key: 'overseasSaudi', label: '🇸🇦 사우디아라비아', color: '#84cc16', count: meCountByCountry('sa') },
|
|
||||||
{ key: 'overseasOman', label: '🇴🇲 오만', color: '#e11d48', count: meCountByCountry('om') },
|
|
||||||
{ key: 'overseasQatar', label: '🇶🇦 카타르', color: '#8b5cf6', count: meCountByCountry('qa') },
|
|
||||||
{ key: 'overseasKuwait', label: '🇰🇼 쿠웨이트', color: '#f97316', count: meCountByCountry('kw') },
|
|
||||||
{ key: 'overseasIraq', label: '🇮🇶 이라크', color: '#65a30d', count: meCountByCountry('iq') },
|
|
||||||
{ key: 'overseasBahrain', label: '🇧🇭 바레인', color: '#e11d48', count: meCountByCountry('bh') },
|
|
||||||
]}
|
|
||||||
hiddenAcCategories={hiddenAcCategories}
|
hiddenAcCategories={hiddenAcCategories}
|
||||||
hiddenShipCategories={hiddenShipCategories}
|
hiddenShipCategories={hiddenShipCategories}
|
||||||
onAcCategoryToggle={toggleAcCategory}
|
onAcCategoryToggle={toggleAcCategory}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocalStorage, useLocalStorageSet } from '../../hooks/useLocalStorage';
|
import { useLocalStorage, useLocalStorageSet } from '../../hooks/useLocalStorage';
|
||||||
import { KoreaMap } from './KoreaMap';
|
import { KoreaMap } from './KoreaMap';
|
||||||
import { FieldAnalysisModal } from './FieldAnalysisModal';
|
import { FieldAnalysisModal } from './FieldAnalysisModal';
|
||||||
import { LayerPanel } from '../common/LayerPanel';
|
import { LayerPanel, type LayerTreeNode } from '../common/LayerPanel';
|
||||||
import { EventLog } from '../common/EventLog';
|
import { EventLog } from '../common/EventLog';
|
||||||
import { LiveControls } from '../common/LiveControls';
|
import { LiveControls } from '../common/LiveControls';
|
||||||
import { ReplayControls } from '../common/ReplayControls';
|
import { ReplayControls } from '../common/ReplayControls';
|
||||||
@ -125,6 +125,14 @@ export const KoreaDashboard = ({
|
|||||||
setKoreaLayers(prev => ({ ...prev, [key]: !prev[key] }));
|
setKoreaLayers(prev => ({ ...prev, [key]: !prev[key] }));
|
||||||
}, [setKoreaLayers]);
|
}, [setKoreaLayers]);
|
||||||
|
|
||||||
|
const batchToggleKoreaLayer = useCallback((keys: string[], value: boolean) => {
|
||||||
|
setKoreaLayers(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
for (const k of keys) next[k] = value;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [setKoreaLayers]);
|
||||||
|
|
||||||
const [hiddenNationalities, setHiddenNationalities] = useLocalStorageSet('hiddenNationalities', new Set());
|
const [hiddenNationalities, setHiddenNationalities] = useLocalStorageSet('hiddenNationalities', new Set());
|
||||||
const toggleNationality = useCallback((nat: string) => {
|
const toggleNationality = useCallback((nat: string) => {
|
||||||
setHiddenNationalities(prev => {
|
setHiddenNationalities(prev => {
|
||||||
@ -164,6 +172,85 @@ export const KoreaDashboard = ({
|
|||||||
// Tab switching is managed by parent (App.tsx); no-op here
|
// Tab switching is managed by parent (App.tsx); no-op here
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const layerTree = useMemo((): LayerTreeNode[] => [
|
||||||
|
{ key: 'ships', label: t('layers.ships'), color: '#fb923c', count: koreaData.ships.length, specialRenderer: 'shipCategories' },
|
||||||
|
{ key: 'nationality', label: '국적 분류', color: '#8b5cf6', count: koreaData.ships.length, specialRenderer: 'nationalityCategories' },
|
||||||
|
{
|
||||||
|
key: 'aviation', label: '항공망', color: '#22d3ee',
|
||||||
|
children: [
|
||||||
|
{ key: 'aircraft', label: t('layers.aircraft'), color: '#22d3ee', count: koreaData.aircraft.length, specialRenderer: 'aircraftCategories' },
|
||||||
|
{ key: 'satellites', label: t('layers.satellites'), color: '#ef4444', count: koreaData.satPositions.length },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maritime-safety', label: '해양안전', color: '#3b82f6',
|
||||||
|
children: [
|
||||||
|
{ key: 'cables', label: t('layers.cables'), color: '#00e5ff', count: KOREA_SUBMARINE_CABLES.length },
|
||||||
|
{ key: 'coastGuard', label: t('layers.coastGuard'), color: '#4dabf7', count: COAST_GUARD_FACILITIES.length },
|
||||||
|
{ key: 'navWarning', label: t('layers.navWarning'), color: '#eab308', count: NAV_WARNINGS.length },
|
||||||
|
{ key: 'osint', label: t('layers.osint'), color: '#ef4444', count: koreaData.osintFeed.length },
|
||||||
|
{ key: 'eez', label: t('layers.eez'), color: '#3b82f6', count: 1 },
|
||||||
|
{ key: 'piracy', label: t('layers.piracy'), color: '#ef4444', count: PIRACY_ZONES.length },
|
||||||
|
{ key: 'nkMissile', label: '미사일 낙하', color: '#ef4444', count: NK_MISSILE_EVENTS.length },
|
||||||
|
{ key: 'nkLaunch', label: '발사/포병진지', color: '#dc2626', count: NK_LAUNCH_SITES.length },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'govt-infra', label: '국가기관망', color: '#f59e0b',
|
||||||
|
children: [
|
||||||
|
{ key: 'ports', label: '항구', color: '#3b82f6', count: EAST_ASIA_PORTS.length },
|
||||||
|
{ key: 'airports', label: t('layers.airports'), color: '#a78bfa', count: KOREAN_AIRPORTS.length },
|
||||||
|
{ key: 'militaryBases', label: '군사시설', color: '#ef4444', count: MILITARY_BASES.length },
|
||||||
|
{ key: 'govBuildings', label: '정부기관', color: '#f59e0b', count: GOV_BUILDINGS.length },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'energy', label: '에너지/발전시설', color: '#a855f7',
|
||||||
|
children: [
|
||||||
|
{ key: 'infra', label: t('layers.infra'), color: '#ffc107' },
|
||||||
|
{ key: 'windFarm', label: '풍력단지', color: '#00bcd4', count: KOREA_WIND_FARMS.length },
|
||||||
|
{ key: 'energyNuclear', label: '원자력발전', color: '#a855f7', count: HAZARD_FACILITIES.filter(f => f.type === 'nuclear').length },
|
||||||
|
{ key: 'energyThermal', label: '화력발전소', color: '#64748b', count: HAZARD_FACILITIES.filter(f => f.type === 'thermal').length },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hazard', label: '위험시설', color: '#ef4444',
|
||||||
|
children: [
|
||||||
|
{ key: 'hazardPetrochemical', label: '해안인접석유화학단지', color: '#f97316', count: HAZARD_FACILITIES.filter(f => f.type === 'petrochemical').length },
|
||||||
|
{ key: 'hazardLng', label: 'LNG저장기지', color: '#06b6d4', count: HAZARD_FACILITIES.filter(f => f.type === 'lng').length },
|
||||||
|
{ key: 'hazardOilTank', label: '유류저장탱크', color: '#eab308', count: HAZARD_FACILITIES.filter(f => f.type === 'oilTank').length },
|
||||||
|
{ key: 'hazardPort', label: '위험물항만하역시설', color: '#ef4444', count: HAZARD_FACILITIES.filter(f => f.type === 'hazardPort').length },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'industry', label: '산업공정/제조시설', color: '#0ea5e9',
|
||||||
|
children: [
|
||||||
|
{ key: 'industryShipyard', label: '조선소 도장시설', color: '#0ea5e9', count: HAZARD_FACILITIES.filter(f => f.type === 'shipyard').length },
|
||||||
|
{ key: 'industryWastewater', label: '폐수/하수처리장', color: '#10b981', count: HAZARD_FACILITIES.filter(f => f.type === 'wastewater').length },
|
||||||
|
{ key: 'industryHeavy', label: '시멘트/제철소', color: '#94a3b8', count: HAZARD_FACILITIES.filter(f => f.type === 'heavyIndustry').length },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'overseas', label: '해외시설', color: '#f97316',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'overseasChina', label: '🇨🇳 중국', color: '#ef4444',
|
||||||
|
children: [
|
||||||
|
{ key: 'cnPower', label: '발전소', color: '#a855f7', count: CN_POWER_PLANTS.length },
|
||||||
|
{ key: 'cnMilitary', label: '주요군사시설', color: '#ef4444', count: CN_MILITARY_FACILITIES.length },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'overseasJapan', label: '🇯🇵 일본', color: '#f472b6',
|
||||||
|
children: [
|
||||||
|
{ key: 'jpPower', label: '발전소', color: '#a855f7', count: JP_POWER_PLANTS.length },
|
||||||
|
{ key: 'jpMilitary', label: '주요군사시설', color: '#ef4444', count: JP_MILITARY_FACILITIES.length },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
], [koreaData, t]);
|
||||||
|
|
||||||
// 헤더 슬롯 Portal — 한국 필터 버튼 + 카운트
|
// 헤더 슬롯 Portal — 한국 필터 버튼 + 카운트
|
||||||
const headerSlot = document.getElementById('dashboard-header-slot');
|
const headerSlot = document.getElementById('dashboard-header-slot');
|
||||||
const countsSlot = document.getElementById('dashboard-counts-slot');
|
const countsSlot = document.getElementById('dashboard-counts-slot');
|
||||||
@ -247,59 +334,13 @@ export const KoreaDashboard = ({
|
|||||||
<LayerPanel
|
<LayerPanel
|
||||||
layers={koreaLayers}
|
layers={koreaLayers}
|
||||||
onToggle={toggleKoreaLayer}
|
onToggle={toggleKoreaLayer}
|
||||||
|
onBatchToggle={batchToggleKoreaLayer}
|
||||||
|
tree={layerTree}
|
||||||
aircraftByCategory={koreaData.aircraftByCategory}
|
aircraftByCategory={koreaData.aircraftByCategory}
|
||||||
aircraftTotal={koreaData.aircraft.length}
|
aircraftTotal={koreaData.aircraft.length}
|
||||||
shipsByMtCategory={koreaData.shipsByCategory}
|
shipsByMtCategory={koreaData.shipsByCategory}
|
||||||
shipTotal={koreaData.ships.length}
|
shipTotal={koreaData.ships.length}
|
||||||
satelliteCount={koreaData.satPositions.length}
|
satelliteCount={koreaData.satPositions.length}
|
||||||
extraLayers={[
|
|
||||||
// 해양안전
|
|
||||||
{ key: 'cables', label: t('layers.cables'), color: '#00e5ff', count: KOREA_SUBMARINE_CABLES.length, group: '해양안전' },
|
|
||||||
{ key: 'coastGuard', label: t('layers.coastGuard'), color: '#4dabf7', count: COAST_GUARD_FACILITIES.length, group: '해양안전' },
|
|
||||||
{ key: 'navWarning', label: t('layers.navWarning'), color: '#eab308', count: NAV_WARNINGS.length, group: '해양안전' },
|
|
||||||
{ key: 'osint', label: t('layers.osint'), color: '#ef4444', count: koreaData.osintFeed.length, group: '해양안전' },
|
|
||||||
{ key: 'eez', label: t('layers.eez'), color: '#3b82f6', count: 1, group: '해양안전' },
|
|
||||||
{ key: 'piracy', label: t('layers.piracy'), color: '#ef4444', count: PIRACY_ZONES.length, group: '해양안전' },
|
|
||||||
{ key: 'nkMissile', label: '🚀 미사일 낙하', color: '#ef4444', count: NK_MISSILE_EVENTS.length, group: '해양안전' },
|
|
||||||
{ key: 'nkLaunch', label: '🇰🇵 발사/포병진지', color: '#dc2626', count: NK_LAUNCH_SITES.length, group: '해양안전' },
|
|
||||||
// 국가기관망
|
|
||||||
{ key: 'ports', label: '항구', color: '#3b82f6', count: EAST_ASIA_PORTS.length, group: '국가기관망' },
|
|
||||||
{ key: 'airports', label: t('layers.airports'), color: '#a78bfa', count: KOREAN_AIRPORTS.length, group: '국가기관망' },
|
|
||||||
{ key: 'militaryBases', label: '군사시설', color: '#ef4444', count: MILITARY_BASES.length, group: '국가기관망' },
|
|
||||||
{ key: 'govBuildings', label: '정부기관', color: '#f59e0b', count: GOV_BUILDINGS.length, group: '국가기관망' },
|
|
||||||
// 에너지/발전시설
|
|
||||||
{ key: 'infra', label: t('layers.infra'), color: '#ffc107', group: '에너지/발전시설' },
|
|
||||||
{ key: 'windFarm', label: '풍력단지', color: '#00bcd4', count: KOREA_WIND_FARMS.length, group: '에너지/발전시설' },
|
|
||||||
{ key: 'energyNuclear', label: '원자력발전', color: '#a855f7', count: HAZARD_FACILITIES.filter(f => f.type === 'nuclear').length, group: '에너지/발전시설' },
|
|
||||||
{ key: 'energyThermal', label: '화력발전소', color: '#64748b', count: HAZARD_FACILITIES.filter(f => f.type === 'thermal').length, group: '에너지/발전시설' },
|
|
||||||
// 위험시설
|
|
||||||
{ key: 'hazardPetrochemical', label: '해안인접석유화학단지', color: '#f97316', count: HAZARD_FACILITIES.filter(f => f.type === 'petrochemical').length, group: '위험시설' },
|
|
||||||
{ key: 'hazardLng', label: 'LNG저장기지', color: '#06b6d4', count: HAZARD_FACILITIES.filter(f => f.type === 'lng').length, group: '위험시설' },
|
|
||||||
{ key: 'hazardOilTank', label: '유류저장탱크', color: '#eab308', count: HAZARD_FACILITIES.filter(f => f.type === 'oilTank').length, group: '위험시설' },
|
|
||||||
{ key: 'hazardPort', label: '위험물항만하역시설', color: '#ef4444', count: HAZARD_FACILITIES.filter(f => f.type === 'hazardPort').length, group: '위험시설' },
|
|
||||||
// 산업공정/제조시설
|
|
||||||
{ key: 'industryShipyard', label: '조선소 도장시설', color: '#0ea5e9', count: HAZARD_FACILITIES.filter(f => f.type === 'shipyard').length, group: '산업공정/제조시설' },
|
|
||||||
{ key: 'industryWastewater', label: '폐수/하수처리장', color: '#10b981', count: HAZARD_FACILITIES.filter(f => f.type === 'wastewater').length, group: '산업공정/제조시설' },
|
|
||||||
{ key: 'industryHeavy', label: '시멘트/제철소', color: '#94a3b8', count: HAZARD_FACILITIES.filter(f => f.type === 'heavyIndustry').length, group: '산업공정/제조시설' },
|
|
||||||
]}
|
|
||||||
overseasItems={[
|
|
||||||
{
|
|
||||||
key: 'overseasChina', label: '🇨🇳 중국', color: '#ef4444',
|
|
||||||
count: CN_POWER_PLANTS.length + CN_MILITARY_FACILITIES.length,
|
|
||||||
children: [
|
|
||||||
{ key: 'cnPower', label: '발전소', color: '#a855f7', count: CN_POWER_PLANTS.length },
|
|
||||||
{ key: 'cnMilitary', label: '주요군사시설', color: '#ef4444', count: CN_MILITARY_FACILITIES.length },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'overseasJapan', label: '🇯🇵 일본', color: '#f472b6',
|
|
||||||
count: JP_POWER_PLANTS.length + JP_MILITARY_FACILITIES.length,
|
|
||||||
children: [
|
|
||||||
{ key: 'jpPower', label: '발전소', color: '#a855f7', count: JP_POWER_PLANTS.length },
|
|
||||||
{ key: 'jpMilitary', label: '주요군사시설', color: '#ef4444', count: JP_MILITARY_FACILITIES.length },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
hiddenAcCategories={hiddenAcCategories}
|
hiddenAcCategories={hiddenAcCategories}
|
||||||
hiddenShipCategories={hiddenShipCategories}
|
hiddenShipCategories={hiddenShipCategories}
|
||||||
onAcCategoryToggle={toggleAcCategory}
|
onAcCategoryToggle={toggleAcCategory}
|
||||||
|
|||||||
@ -59,9 +59,236 @@ function infraSvg(f: PowerFacility): string {
|
|||||||
</svg>`;
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Hazard SVG ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function nuclearSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<circle cx="12" cy="12" r="2" fill="${color}"/>
|
||||||
|
<path d="M12 10 Q10 7 7 7 Q6 9 7 11 Q9 12 12 12" fill="${color}" opacity="0.7"/>
|
||||||
|
<path d="M13.7 11 Q16 9 17 7 Q15 5 13 6 Q11 8 12 10" fill="${color}" opacity="0.7"/>
|
||||||
|
<path d="M10.3 13 Q7 13 6 16 Q8 18 11 17 Q13 15 13.7 13" fill="${color}" opacity="0.7"/>
|
||||||
|
<path d="M13.7 13 Q15 16 17 17 Q19 15 18 12 Q16 11 13.7 12" fill="${color}" opacity="0.7"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function thermalSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<rect x="5" y="11" width="14" height="7" rx="1" fill="${color}" opacity="0.6"/>
|
||||||
|
<rect x="7" y="7" width="2" height="5" fill="${color}" opacity="0.6"/>
|
||||||
|
<rect x="11" y="5" width="2" height="7" fill="${color}" opacity="0.6"/>
|
||||||
|
<rect x="15" y="8" width="2" height="4" fill="${color}" opacity="0.6"/>
|
||||||
|
<path d="M8 5 Q8.5 3.5 9 5 Q9.5 3 10 5" fill="none" stroke="${color}" stroke-width="0.8" opacity="0.8"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function petrochemSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<rect x="10" y="5" width="4" height="8" rx="1" fill="${color}" opacity="0.65"/>
|
||||||
|
<ellipse cx="12" cy="14.5" rx="4.5" ry="2.5" fill="${color}" opacity="0.75"/>
|
||||||
|
<line x1="7" y1="10" x2="10" y2="10" stroke="${color}" stroke-width="1"/>
|
||||||
|
<line x1="14" y1="10" x2="17" y2="10" stroke="${color}" stroke-width="1"/>
|
||||||
|
<path d="M7 10 Q5.5 13 8 15" fill="none" stroke="${color}" stroke-width="0.8"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lngSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<line x1="9" y1="7" x2="9" y2="10" stroke="${color}" stroke-width="1.5"/>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="10" stroke="${color}" stroke-width="1.5"/>
|
||||||
|
<line x1="15" y1="7" x2="15" y2="10" stroke="${color}" stroke-width="1.5"/>
|
||||||
|
<line x1="7" y1="9" x2="17" y2="9" stroke="${color}" stroke-width="1"/>
|
||||||
|
<ellipse cx="12" cy="15" rx="5" ry="3.5" fill="${color}" opacity="0.65"/>
|
||||||
|
<line x1="12" y1="10" x2="12" y2="11.5" stroke="${color}" stroke-width="1"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function oilTankSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<ellipse cx="12" cy="8" rx="5" ry="2" fill="${color}" opacity="0.5"/>
|
||||||
|
<rect x="7" y="8" width="10" height="8" fill="${color}" opacity="0.6"/>
|
||||||
|
<ellipse cx="12" cy="16" rx="5" ry="2" fill="${color}" opacity="0.8"/>
|
||||||
|
<line x1="9" y1="8" x2="9" y2="16" stroke="rgba(0,0,0,0.2)" stroke-width="0.5"/>
|
||||||
|
<line x1="15" y1="8" x2="15" y2="16" stroke="rgba(0,0,0,0.2)" stroke-width="0.5"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hazPortSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<path d="M12 5 L20 18 L4 18 Z" fill="${color}" opacity="0.7"/>
|
||||||
|
<line x1="12" y1="10" x2="12" y2="14" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/>
|
||||||
|
<circle cx="12" cy="16" r="1" fill="#fff"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shipyardSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<path d="M6 15 Q7 13 9 13 L15 13 Q17 13 18 15 L17 17 Q12 19 7 17 Z" fill="${color}" opacity="0.75"/>
|
||||||
|
<line x1="12" y1="6" x2="12" y2="13" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<line x1="12" y1="6" x2="16" y2="10" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<line x1="16" y1="10" x2="16" y2="13" stroke="${color}" stroke-width="1"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wastewaterSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<path d="M12 5 Q15 9 15 12 Q15 15.3 12 17 Q9 15.3 9 12 Q9 9 12 5 Z" fill="${color}" opacity="0.75"/>
|
||||||
|
<path d="M9 14 Q10.5 15.5 12 16" fill="none" stroke="#fff" stroke-width="0.7" opacity="0.5"/>
|
||||||
|
<path d="M8.5 12 Q9.5 13.5 11 14" fill="none" stroke="#fff" stroke-width="0.7" opacity="0.5"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function heavyIndustrySvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<circle cx="12" cy="12" r="4" fill="none" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<circle cx="12" cy="12" r="1.5" fill="${color}"/>
|
||||||
|
<line x1="12" y1="6" x2="12" y2="8" stroke="${color}" stroke-width="1.5"/>
|
||||||
|
<line x1="12" y1="16" x2="12" y2="18" stroke="${color}" stroke-width="1.5"/>
|
||||||
|
<line x1="6" y1="12" x2="8" y2="12" stroke="${color}" stroke-width="1.5"/>
|
||||||
|
<line x1="16" y1="12" x2="18" y2="12" stroke="${color}" stroke-width="1.5"/>
|
||||||
|
<line x1="7.8" y1="7.8" x2="9.2" y2="9.2" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<line x1="14.8" y1="14.8" x2="16.2" y2="16.2" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<line x1="16.2" y1="7.8" x2="14.8" y2="9.2" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<line x1="9.2" y1="14.8" x2="7.8" y2="16.2" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Naval/Airbase/Army SVG (reused from createMilitaryLayers pattern) ─────────
|
||||||
|
|
||||||
|
function navalFacSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" stroke="${color}" stroke-width="1.5"/>
|
||||||
|
<line x1="7" y1="9" x2="17" y2="9" stroke="${color}" stroke-width="1.5"/>
|
||||||
|
<path d="M7 9 Q6 13 8 15" fill="none" stroke="${color}" stroke-width="1"/>
|
||||||
|
<path d="M17 9 Q18 13 16 15" fill="none" stroke="${color}" stroke-width="1"/>
|
||||||
|
<path d="M8 15 Q12 18 16 15" fill="none" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<circle cx="12" cy="5" r="1.5" fill="${color}"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function airbaseFacSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<path d="M12 5 C11.4 5 11 5.4 11 5.8 L11 10 L6 13 L6 14.2 L11 12.5 L11 16.5 L9.2 17.8 L9.2 18.8 L12 18 L14.8 18.8 L14.8 17.8 L13 16.5 L13 12.5 L18 14.2 L18 13 L13 10 L13 5.8 C13 5.4 12.6 5 12 5Z"
|
||||||
|
fill="${color}" stroke="#fff" stroke-width="0.3"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function armyFacSvg(color: string, size: number): string {
|
||||||
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
||||||
|
<path d="M6 10 Q6 8 8 8 L12 7 L16 8 Q18 8 18 10 L18 12 Q18 15 12 17 Q6 15 6 12 Z" fill="${color}" opacity="0.75"/>
|
||||||
|
<line x1="9" y1="10" x2="15" y2="10" stroke="#fff" stroke-width="0.8" opacity="0.5"/>
|
||||||
|
<line x1="8.5" y1="12" x2="15.5" y2="12" stroke="#fff" stroke-width="0.8" opacity="0.5"/>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Module-level icon caches ─────────────────────────────────────────────────
|
// ─── Module-level icon caches ─────────────────────────────────────────────────
|
||||||
|
|
||||||
const infraIconCache = new Map<string, string>();
|
const infraIconCache = new Map<string, string>();
|
||||||
|
const hazardIconCache = new Map<string, string>();
|
||||||
|
const cnIconCache = new Map<string, string>();
|
||||||
|
const jpIconCache = new Map<string, string>();
|
||||||
|
|
||||||
|
// ─── Hazard icon helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const HAZARD_SVG: Record<string, (c: string, s: number) => string> = {
|
||||||
|
petrochemical: petrochemSvg,
|
||||||
|
lng: lngSvg,
|
||||||
|
oilTank: oilTankSvg,
|
||||||
|
hazardPort: hazPortSvg,
|
||||||
|
nuclear: nuclearSvg,
|
||||||
|
thermal: thermalSvg,
|
||||||
|
shipyard: shipyardSvg,
|
||||||
|
wastewater: wastewaterSvg,
|
||||||
|
heavyIndustry: heavyIndustrySvg,
|
||||||
|
};
|
||||||
|
|
||||||
|
const HAZARD_COLOR: Record<string, string> = {
|
||||||
|
petrochemical: '#f97316',
|
||||||
|
lng: '#06b6d4',
|
||||||
|
oilTank: '#eab308',
|
||||||
|
hazardPort: '#ef4444',
|
||||||
|
nuclear: '#a855f7',
|
||||||
|
thermal: '#64748b',
|
||||||
|
shipyard: '#0ea5e9',
|
||||||
|
wastewater: '#10b981',
|
||||||
|
heavyIndustry: '#94a3b8',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getHazardIconUrl(type: string): string {
|
||||||
|
if (!hazardIconCache.has(type)) {
|
||||||
|
const color = HAZARD_COLOR[type] ?? '#888';
|
||||||
|
const svgFn = HAZARD_SVG[type] ?? hazPortSvg;
|
||||||
|
hazardIconCache.set(type, svgToDataUri(svgFn(color, 64)));
|
||||||
|
}
|
||||||
|
return hazardIconCache.get(type)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── CN icon helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const CN_SVG: Record<string, (c: string, s: number) => string> = {
|
||||||
|
nuclear: nuclearSvg,
|
||||||
|
thermal: thermalSvg,
|
||||||
|
naval: navalFacSvg,
|
||||||
|
airbase: airbaseFacSvg,
|
||||||
|
army: armyFacSvg,
|
||||||
|
shipyard: shipyardSvg,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CN_COLOR: Record<string, string> = {
|
||||||
|
nuclear: '#ef4444',
|
||||||
|
thermal: '#f97316',
|
||||||
|
naval: '#3b82f6',
|
||||||
|
airbase: '#22d3ee',
|
||||||
|
army: '#22c55e',
|
||||||
|
shipyard: '#94a3b8',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCnIconUrl(subType: string): string {
|
||||||
|
if (!cnIconCache.has(subType)) {
|
||||||
|
const color = CN_COLOR[subType] ?? '#888';
|
||||||
|
const svgFn = CN_SVG[subType] ?? armyFacSvg;
|
||||||
|
cnIconCache.set(subType, svgToDataUri(svgFn(color, 64)));
|
||||||
|
}
|
||||||
|
return cnIconCache.get(subType)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── JP icon helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const JP_SVG: Record<string, (c: string, s: number) => string> = {
|
||||||
|
nuclear: nuclearSvg,
|
||||||
|
thermal: thermalSvg,
|
||||||
|
naval: navalFacSvg,
|
||||||
|
airbase: airbaseFacSvg,
|
||||||
|
army: armyFacSvg,
|
||||||
|
};
|
||||||
|
|
||||||
|
const JP_COLOR: Record<string, string> = {
|
||||||
|
nuclear: '#ef4444',
|
||||||
|
thermal: '#f97316',
|
||||||
|
naval: '#3b82f6',
|
||||||
|
airbase: '#22d3ee',
|
||||||
|
army: '#22c55e',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getJpIconUrl(subType: string): string {
|
||||||
|
if (!jpIconCache.has(subType)) {
|
||||||
|
const color = JP_COLOR[subType] ?? '#888';
|
||||||
|
const svgFn = JP_SVG[subType] ?? armyFacSvg;
|
||||||
|
jpIconCache.set(subType, svgToDataUri(svgFn(color, 64)));
|
||||||
|
}
|
||||||
|
return jpIconCache.get(subType)!;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── createFacilityLayers ─────────────────────────────────────────────────────
|
// ─── createFacilityLayers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -148,37 +375,32 @@ export function createFacilityLayers(
|
|||||||
const hazardTypeSet = new Set(config.hazardTypes);
|
const hazardTypeSet = new Set(config.hazardTypes);
|
||||||
const hazardData = HAZARD_FACILITIES.filter(f => hazardTypeSet.has(f.type));
|
const hazardData = HAZARD_FACILITIES.filter(f => hazardTypeSet.has(f.type));
|
||||||
|
|
||||||
const HAZARD_META: Record<string, { icon: string; color: [number, number, number, number] }> = {
|
const HAZARD_META: Record<string, { color: [number, number, number, number] }> = {
|
||||||
petrochemical: { icon: '🏭', color: [249, 115, 22, 255] },
|
petrochemical: { color: [249, 115, 22, 255] },
|
||||||
lng: { icon: '🔵', color: [6, 182, 212, 255] },
|
lng: { color: [6, 182, 212, 255] },
|
||||||
oilTank: { icon: '🛢️', color: [234, 179, 8, 255] },
|
oilTank: { color: [234, 179, 8, 255] },
|
||||||
hazardPort: { icon: '⚠️', color: [239, 68, 68, 255] },
|
hazardPort: { color: [239, 68, 68, 255] },
|
||||||
nuclear: { icon: '☢️', color: [168, 85, 247, 255] },
|
nuclear: { color: [168, 85, 247, 255] },
|
||||||
thermal: { icon: '🔥', color: [100, 116, 139, 255] },
|
thermal: { color: [100, 116, 139, 255] },
|
||||||
shipyard: { icon: '🚢', color: [14, 165, 233, 255] },
|
shipyard: { color: [14, 165, 233, 255] },
|
||||||
wastewater: { icon: '💧', color: [16, 185, 129, 255] },
|
wastewater: { color: [16, 185, 129, 255] },
|
||||||
heavyIndustry: { icon: '⚙️', color: [148, 163, 184, 255] },
|
heavyIndustry: { color: [148, 163, 184, 255] },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hazardData.length > 0) {
|
if (hazardData.length > 0) {
|
||||||
layers.push(
|
layers.push(
|
||||||
new TextLayer<HazardFacility>({
|
new IconLayer<HazardFacility>({
|
||||||
id: 'static-hazard-emoji',
|
id: 'static-hazard-icon',
|
||||||
data: hazardData,
|
data: hazardData,
|
||||||
getPosition: (d) => [d.lng, d.lat],
|
getPosition: (d) => [d.lng, d.lat],
|
||||||
getText: (d) => HAZARD_META[d.type]?.icon ?? '⚠️',
|
getIcon: (d) => ({ url: getHazardIconUrl(d.type), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
|
||||||
getSize: 16 * sc,
|
getSize: 18 * sc,
|
||||||
updateTriggers: { getSize: [sc] },
|
updateTriggers: { getSize: [sc] },
|
||||||
getColor: (d) => HAZARD_META[d.type]?.color ?? [200, 200, 200, 255],
|
|
||||||
getTextAnchor: 'middle',
|
|
||||||
getAlignmentBaseline: 'center',
|
|
||||||
pickable: true,
|
pickable: true,
|
||||||
onClick: (info: PickingInfo<HazardFacility>) => {
|
onClick: (info: PickingInfo<HazardFacility>) => {
|
||||||
if (info.object) onPick({ kind: 'hazard', object: info.object });
|
if (info.object) onPick({ kind: 'hazard', object: info.object });
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
billboard: false,
|
|
||||||
characterSet: 'auto',
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
layers.push(
|
layers.push(
|
||||||
@ -196,7 +418,7 @@ export function createFacilityLayers(
|
|||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSettings: { sdf: true },
|
fontSettings: { sdf: true },
|
||||||
outlineWidth: 8,
|
outlineWidth: 8,
|
||||||
outlineColor: [0, 0, 0, 255],
|
outlineColor: [0, 0, 0, 255],
|
||||||
billboard: false,
|
billboard: false,
|
||||||
characterSet: 'auto',
|
characterSet: 'auto',
|
||||||
@ -207,13 +429,13 @@ export function createFacilityLayers(
|
|||||||
|
|
||||||
// ── CN Facilities ──────────────────────────────────────────────────────
|
// ── CN Facilities ──────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
const CN_META: Record<string, { icon: string; color: [number, number, number, number] }> = {
|
const CN_META: Record<string, { color: [number, number, number, number] }> = {
|
||||||
nuclear: { icon: '☢️', color: [239, 68, 68, 255] },
|
nuclear: { color: [239, 68, 68, 255] },
|
||||||
thermal: { icon: '🔥', color: [249, 115, 22, 255] },
|
thermal: { color: [249, 115, 22, 255] },
|
||||||
naval: { icon: '⚓', color: [59, 130, 246, 255] },
|
naval: { color: [59, 130, 246, 255] },
|
||||||
airbase: { icon: '✈️', color: [34, 211, 238, 255] },
|
airbase: { color: [34, 211, 238, 255] },
|
||||||
army: { icon: '🪖', color: [34, 197, 94, 255] },
|
army: { color: [34, 197, 94, 255] },
|
||||||
shipyard: { icon: '🚢', color: [148, 163, 184, 255] },
|
shipyard: { color: [148, 163, 184, 255] },
|
||||||
};
|
};
|
||||||
const cnData: CnFacility[] = [
|
const cnData: CnFacility[] = [
|
||||||
...(config.cnPower ? CN_POWER_PLANTS : []),
|
...(config.cnPower ? CN_POWER_PLANTS : []),
|
||||||
@ -221,23 +443,18 @@ export function createFacilityLayers(
|
|||||||
];
|
];
|
||||||
if (cnData.length > 0) {
|
if (cnData.length > 0) {
|
||||||
layers.push(
|
layers.push(
|
||||||
new TextLayer<CnFacility>({
|
new IconLayer<CnFacility>({
|
||||||
id: 'static-cn-emoji',
|
id: 'static-cn-icon',
|
||||||
data: cnData,
|
data: cnData,
|
||||||
getPosition: (d) => [d.lng, d.lat],
|
getPosition: (d) => [d.lng, d.lat],
|
||||||
getText: (d) => CN_META[d.subType]?.icon ?? '📍',
|
getIcon: (d) => ({ url: getCnIconUrl(d.subType), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
|
||||||
getSize: 16 * sc,
|
getSize: 18 * sc,
|
||||||
updateTriggers: { getSize: [sc] },
|
updateTriggers: { getSize: [sc] },
|
||||||
getColor: (d) => CN_META[d.subType]?.color ?? [200, 200, 200, 255],
|
|
||||||
getTextAnchor: 'middle',
|
|
||||||
getAlignmentBaseline: 'center',
|
|
||||||
pickable: true,
|
pickable: true,
|
||||||
onClick: (info: PickingInfo<CnFacility>) => {
|
onClick: (info: PickingInfo<CnFacility>) => {
|
||||||
if (info.object) onPick({ kind: 'cnFacility', object: info.object });
|
if (info.object) onPick({ kind: 'cnFacility', object: info.object });
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
billboard: false,
|
|
||||||
characterSet: 'auto',
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
layers.push(
|
layers.push(
|
||||||
@ -255,7 +472,7 @@ export function createFacilityLayers(
|
|||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSettings: { sdf: true },
|
fontSettings: { sdf: true },
|
||||||
outlineWidth: 8,
|
outlineWidth: 8,
|
||||||
outlineColor: [0, 0, 0, 255],
|
outlineColor: [0, 0, 0, 255],
|
||||||
billboard: false,
|
billboard: false,
|
||||||
characterSet: 'auto',
|
characterSet: 'auto',
|
||||||
@ -266,12 +483,12 @@ export function createFacilityLayers(
|
|||||||
|
|
||||||
// ── JP Facilities ──────────────────────────────────────────────────────
|
// ── JP Facilities ──────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
const JP_META: Record<string, { icon: string; color: [number, number, number, number] }> = {
|
const JP_META: Record<string, { color: [number, number, number, number] }> = {
|
||||||
nuclear: { icon: '☢️', color: [239, 68, 68, 255] },
|
nuclear: { color: [239, 68, 68, 255] },
|
||||||
thermal: { icon: '🔥', color: [249, 115, 22, 255] },
|
thermal: { color: [249, 115, 22, 255] },
|
||||||
naval: { icon: '⚓', color: [59, 130, 246, 255] },
|
naval: { color: [59, 130, 246, 255] },
|
||||||
airbase: { icon: '✈️', color: [34, 211, 238, 255] },
|
airbase: { color: [34, 211, 238, 255] },
|
||||||
army: { icon: '🪖', color: [34, 197, 94, 255] },
|
army: { color: [34, 197, 94, 255] },
|
||||||
};
|
};
|
||||||
const jpData: JpFacility[] = [
|
const jpData: JpFacility[] = [
|
||||||
...(config.jpPower ? JP_POWER_PLANTS : []),
|
...(config.jpPower ? JP_POWER_PLANTS : []),
|
||||||
@ -279,23 +496,18 @@ export function createFacilityLayers(
|
|||||||
];
|
];
|
||||||
if (jpData.length > 0) {
|
if (jpData.length > 0) {
|
||||||
layers.push(
|
layers.push(
|
||||||
new TextLayer<JpFacility>({
|
new IconLayer<JpFacility>({
|
||||||
id: 'static-jp-emoji',
|
id: 'static-jp-icon',
|
||||||
data: jpData,
|
data: jpData,
|
||||||
getPosition: (d) => [d.lng, d.lat],
|
getPosition: (d) => [d.lng, d.lat],
|
||||||
getText: (d) => JP_META[d.subType]?.icon ?? '📍',
|
getIcon: (d) => ({ url: getJpIconUrl(d.subType), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
|
||||||
getSize: 16 * sc,
|
getSize: 18 * sc,
|
||||||
updateTriggers: { getSize: [sc] },
|
updateTriggers: { getSize: [sc] },
|
||||||
getColor: (d) => JP_META[d.subType]?.color ?? [200, 200, 200, 255],
|
|
||||||
getTextAnchor: 'middle',
|
|
||||||
getAlignmentBaseline: 'center',
|
|
||||||
pickable: true,
|
pickable: true,
|
||||||
onClick: (info: PickingInfo<JpFacility>) => {
|
onClick: (info: PickingInfo<JpFacility>) => {
|
||||||
if (info.object) onPick({ kind: 'jpFacility', object: info.object });
|
if (info.object) onPick({ kind: 'jpFacility', object: info.object });
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
billboard: false,
|
|
||||||
characterSet: 'auto',
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
layers.push(
|
layers.push(
|
||||||
@ -313,7 +525,7 @@ export function createFacilityLayers(
|
|||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSettings: { sdf: true },
|
fontSettings: { sdf: true },
|
||||||
outlineWidth: 8,
|
outlineWidth: 8,
|
||||||
outlineColor: [0, 0, 0, 255],
|
outlineColor: [0, 0, 0, 255],
|
||||||
billboard: false,
|
billboard: false,
|
||||||
characterSet: 'auto',
|
characterSet: 'auto',
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user