feat: 지도 글꼴 크기 커스텀 시스템 (4개 그룹 슬라이더)
- FontScaleContext + FontScalePanel: 시설/선박/분석/지역 4그룹 × 0.5~2.0 범위 - LAYERS 패널 하단 슬라이더 UI, localStorage 영속화 - Korea static 14개 + Iran 4개 + 분석 3개 + KoreaMap 5개 TextLayer 적용 - MapLibre 선박 라벨/국가명 실시간 반영 - 모든 useMemo deps + updateTriggers에 fontScale 포함
This commit is contained in:
부모
6d4ac4d3fe
커밋
44aa449b03
@ -4,6 +4,20 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 추가
|
||||
- LayerPanel 공통 트리 구조: LayerTreeNode 재귀 렌더러 (한국/이란 양쪽 적용)
|
||||
- 위험시설/해외시설 emoji→SVG IconLayer 전환 (12 SVG 함수, hazard/CN/JP 3개 IconLayer)
|
||||
- S&P Global 피격 선박 27척 데이터 (damagedShips.ts)
|
||||
- 이란 리플레이 실데이터 전환: Backend 시점 조회 API + Events CRUD
|
||||
- GeoEvent `sea_attack` 타입 + SEA ATK 배지 (피격 선박 이벤트 로그 통합)
|
||||
- 더미↔API 토글 UI (리플레이 배속 우측)
|
||||
- 대시보드 탭 localStorage 영속화
|
||||
|
||||
### 변경
|
||||
- 부모 노드 토글→하위 전체 ON/OFF 캐스케이드 + 카운트 합산
|
||||
- useIranData dataSource 분기 (dummy=sampleData, api=Backend DB 3월1일~오늘)
|
||||
- fetchAircraftByRange, fetchOsintByRange, fetchEventsByRange 서비스 함수
|
||||
|
||||
## [2026-03-23.6]
|
||||
|
||||
### 수정
|
||||
|
||||
@ -2462,3 +2462,42 @@
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ── FontScalePanel ──────────────────────── */
|
||||
.font-scale-section { margin-top: 4px; }
|
||||
.font-scale-toggle {
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
color: var(--kcg-text);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255,255,255,0.08);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.font-scale-toggle:hover { background: rgba(255,255,255,0.05); }
|
||||
.font-scale-sliders { padding: 4px 8px; }
|
||||
.font-scale-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 9px;
|
||||
color: var(--kcg-dim);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.font-scale-row label { width: 60px; flex-shrink: 0; }
|
||||
.font-scale-row input[type="range"] { flex: 1; height: 12px; accent-color: var(--kcg-primary, #3b82f6); }
|
||||
.font-scale-row span { width: 24px; text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.font-scale-reset {
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
font-size: 9px;
|
||||
color: var(--kcg-dim);
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import LoginPage from './components/auth/LoginPage';
|
||||
import CollectorMonitor from './components/common/CollectorMonitor';
|
||||
import { SharedFilterProvider } from './contexts/SharedFilterContext';
|
||||
import { FontScaleProvider } from './contexts/FontScaleContext';
|
||||
import { IranDashboard } from './components/iran/IranDashboard';
|
||||
import { KoreaDashboard } from './components/korea/KoreaDashboard';
|
||||
import './App.css';
|
||||
@ -65,6 +66,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
|
||||
const currentTime = isLive ? monitor.state.currentTime : replay.state.currentTime;
|
||||
|
||||
return (
|
||||
<FontScaleProvider>
|
||||
<SharedFilterProvider>
|
||||
<div className={`app ${isLive ? 'app-live' : ''}`}>
|
||||
<header className="app-header">
|
||||
@ -158,6 +160,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
|
||||
)}
|
||||
</div>
|
||||
</SharedFilterProvider>
|
||||
</FontScaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
45
frontend/src/components/common/FontScalePanel.tsx
Normal file
45
frontend/src/components/common/FontScalePanel.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useState } from 'react';
|
||||
import { useFontScale } from '../../hooks/useFontScale';
|
||||
import type { FontScaleConfig } from '../../contexts/fontScaleState';
|
||||
|
||||
const LABELS: Record<keyof FontScaleConfig, string> = {
|
||||
facility: '시설 라벨',
|
||||
ship: '선박 이름',
|
||||
analysis: '분석 라벨',
|
||||
area: '지역/국가명',
|
||||
};
|
||||
|
||||
export function FontScalePanel() {
|
||||
const { fontScale, setFontScale } = useFontScale();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const update = (key: keyof FontScaleConfig, val: number) => {
|
||||
setFontScale({ ...fontScale, [key]: Math.round(val * 10) / 10 });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="font-scale-section">
|
||||
<button type="button" className="font-scale-toggle" onClick={() => setOpen(!open)}>
|
||||
<span>Aa 글꼴 크기</span>
|
||||
<span>{open ? '▼' : '▶'}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="font-scale-sliders">
|
||||
{(Object.keys(LABELS) as (keyof FontScaleConfig)[]).map(key => (
|
||||
<div key={key} className="font-scale-row">
|
||||
<label>{LABELS[key]}</label>
|
||||
<input type="range" min={0.5} max={2.0} step={0.1}
|
||||
value={fontScale[key]}
|
||||
onChange={e => update(key, parseFloat(e.target.value))} />
|
||||
<span>{fontScale[key].toFixed(1)}</span>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="font-scale-reset"
|
||||
onClick={() => setFontScale({ facility: 1.0, ship: 1.0, analysis: 1.0, area: 1.0 })}>
|
||||
초기화
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocalStorageSet } from '../../hooks/useLocalStorage';
|
||||
import { FontScalePanel } from './FontScalePanel';
|
||||
|
||||
// Aircraft category colors (matches AircraftLayer military fixed colors)
|
||||
const AC_CAT_COLORS: Record<string, string> = {
|
||||
@ -895,6 +896,7 @@ export function LayerPanel({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<FontScalePanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ export { layerKeyToSubType, layerKeyToCountry };
|
||||
export interface MELayerConfig {
|
||||
layers: Record<string, boolean>;
|
||||
sc: number;
|
||||
fs?: number;
|
||||
onPick: (facility: EnergyHazardFacility) => void;
|
||||
}
|
||||
|
||||
@ -174,6 +175,7 @@ function getIconUrl(subType: FacilitySubType): string {
|
||||
|
||||
export function createMEEnergyHazardLayers(config: MELayerConfig): Layer[] {
|
||||
const { layers, sc, onPick } = config;
|
||||
const fs = config.fs ?? 1;
|
||||
|
||||
const visibleFacilities = ME_ENERGY_HAZARD_FACILITIES.filter(f =>
|
||||
isFacilityVisible(f, layers),
|
||||
@ -200,7 +202,7 @@ export function createMEEnergyHazardLayers(config: MELayerConfig): Layer[] {
|
||||
data: visibleFacilities,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo,
|
||||
getSize: 12 * sc,
|
||||
getSize: 12 * sc * fs,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getColor: (d) => hexToRgba(SUB_TYPE_META[d.subType].color, 200),
|
||||
getTextAnchor: 'middle',
|
||||
|
||||
@ -8,6 +8,7 @@ import { ShipLayer } from '../layers/ShipLayer';
|
||||
import { DamagedShipLayer } from '../layers/DamagedShipLayer';
|
||||
import { SeismicMarker } from '../layers/SeismicMarker';
|
||||
import { DeckGLOverlay } from '../layers/DeckGLOverlay';
|
||||
import { useFontScale } from '../../hooks/useFontScale';
|
||||
import { createMEEnergyHazardLayers } from './MEEnergyHazardLayer';
|
||||
import type { EnergyHazardFacility } from '../../data/meEnergyHazardFacilities';
|
||||
import { SUB_TYPE_META } from '../../data/meEnergyHazardFacilities';
|
||||
@ -128,6 +129,7 @@ export function ReplayMap({ events, currentTime, aircraft, satellites, ships, la
|
||||
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
|
||||
const [mePickedFacility, setMePickedFacility] = useState<EnergyHazardFacility | null>(null);
|
||||
const [iranPickedFacility, setIranPickedFacility] = useState<IranPickedFacility | null>(null);
|
||||
const { fontScale } = useFontScale();
|
||||
const [zoomLevel, setZoomLevel] = useState(5);
|
||||
const zoomRef = useRef(5);
|
||||
|
||||
@ -154,11 +156,11 @@ export function ReplayMap({ events, currentTime, aircraft, satellites, ships, la
|
||||
}, [zoomLevel]);
|
||||
|
||||
const iranDeckLayers = useMemo(() => [
|
||||
...createIranOilLayers({ visible: layers.oilFacilities, sc: zoomScale, onPick: (f) => setIranPickedFacility({ kind: 'oil', data: f }) }),
|
||||
...createIranAirportLayers({ visible: layers.airports, sc: zoomScale, onPick: (f) => setIranPickedFacility({ kind: 'airport', data: f }) }),
|
||||
...createMEFacilityLayers({ visible: layers.meFacilities, sc: zoomScale, onPick: (f) => setIranPickedFacility({ kind: 'meFacility', data: f }) }),
|
||||
...createMEEnergyHazardLayers({ layers: layers as Record<string, boolean>, sc: zoomScale, onPick: setMePickedFacility }),
|
||||
], [layers, zoomScale]);
|
||||
...createIranOilLayers({ visible: layers.oilFacilities, sc: zoomScale, fs: fontScale.facility, onPick: (f) => setIranPickedFacility({ kind: 'oil', data: f }) }),
|
||||
...createIranAirportLayers({ visible: layers.airports, sc: zoomScale, fs: fontScale.facility, onPick: (f) => setIranPickedFacility({ kind: 'airport', data: f }) }),
|
||||
...createMEFacilityLayers({ visible: layers.meFacilities, sc: zoomScale, fs: fontScale.facility, onPick: (f) => setIranPickedFacility({ kind: 'meFacility', data: f }) }),
|
||||
...createMEEnergyHazardLayers({ layers: layers as Record<string, boolean>, sc: zoomScale, fs: fontScale.facility, onPick: setMePickedFacility }),
|
||||
], [layers, zoomScale, fontScale.facility]);
|
||||
|
||||
useEffect(() => {
|
||||
if (flyToTarget && mapRef.current) {
|
||||
@ -242,7 +244,7 @@ export function ReplayMap({ events, currentTime, aircraft, satellites, ships, la
|
||||
filter={['==', ['get', 'rank'], 1]}
|
||||
layout={{
|
||||
'text-field': ['get', 'name'],
|
||||
'text-size': 15,
|
||||
'text-size': 15 * fontScale.area,
|
||||
'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'],
|
||||
'text-allow-overlap': false,
|
||||
'text-ignore-placement': false,
|
||||
@ -261,7 +263,7 @@ export function ReplayMap({ events, currentTime, aircraft, satellites, ships, la
|
||||
filter={['==', ['get', 'rank'], 2]}
|
||||
layout={{
|
||||
'text-field': ['get', 'name'],
|
||||
'text-size': 12,
|
||||
'text-size': 12 * fontScale.area,
|
||||
'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'],
|
||||
'text-allow-overlap': false,
|
||||
'text-ignore-placement': false,
|
||||
@ -281,7 +283,7 @@ export function ReplayMap({ events, currentTime, aircraft, satellites, ships, la
|
||||
minzoom={5}
|
||||
layout={{
|
||||
'text-field': ['get', 'name'],
|
||||
'text-size': 10,
|
||||
'text-size': 10 * fontScale.area,
|
||||
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
|
||||
'text-allow-overlap': false,
|
||||
'text-ignore-placement': false,
|
||||
|
||||
@ -8,6 +8,7 @@ import { ShipLayer } from '../layers/ShipLayer';
|
||||
import { DamagedShipLayer } from '../layers/DamagedShipLayer';
|
||||
import { SeismicMarker } from '../layers/SeismicMarker';
|
||||
import { DeckGLOverlay } from '../layers/DeckGLOverlay';
|
||||
import { useFontScale } from '../../hooks/useFontScale';
|
||||
import { createMEEnergyHazardLayers } from './MEEnergyHazardLayer';
|
||||
import type { EnergyHazardFacility } from '../../data/meEnergyHazardFacilities';
|
||||
import { SUB_TYPE_META } from '../../data/meEnergyHazardFacilities';
|
||||
@ -111,6 +112,7 @@ export function SatelliteMap({ events, currentTime, aircraft, satellites, ships,
|
||||
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
|
||||
const [mePickedFacility, setMePickedFacility] = useState<EnergyHazardFacility | null>(null);
|
||||
const [iranPickedFacility, setIranPickedFacility] = useState<IranPickedFacility | null>(null);
|
||||
const { fontScale } = useFontScale();
|
||||
const [zoomLevel, setZoomLevel] = useState(5);
|
||||
const zoomRef = useRef(5);
|
||||
|
||||
@ -137,11 +139,11 @@ export function SatelliteMap({ events, currentTime, aircraft, satellites, ships,
|
||||
}, [zoomLevel]);
|
||||
|
||||
const iranDeckLayers = useMemo(() => [
|
||||
...createIranOilLayers({ visible: layers.oilFacilities, sc: zoomScale, onPick: (f) => setIranPickedFacility({ kind: 'oil', data: f }) }),
|
||||
...createIranAirportLayers({ visible: layers.airports, sc: zoomScale, onPick: (f) => setIranPickedFacility({ kind: 'airport', data: f }) }),
|
||||
...createMEFacilityLayers({ visible: layers.meFacilities, sc: zoomScale, onPick: (f) => setIranPickedFacility({ kind: 'meFacility', data: f }) }),
|
||||
...createMEEnergyHazardLayers({ layers: layers as Record<string, boolean>, sc: zoomScale, onPick: setMePickedFacility }),
|
||||
], [layers, zoomScale]);
|
||||
...createIranOilLayers({ visible: layers.oilFacilities, sc: zoomScale, fs: fontScale.facility, onPick: (f) => setIranPickedFacility({ kind: 'oil', data: f }) }),
|
||||
...createIranAirportLayers({ visible: layers.airports, sc: zoomScale, fs: fontScale.facility, onPick: (f) => setIranPickedFacility({ kind: 'airport', data: f }) }),
|
||||
...createMEFacilityLayers({ visible: layers.meFacilities, sc: zoomScale, fs: fontScale.facility, onPick: (f) => setIranPickedFacility({ kind: 'meFacility', data: f }) }),
|
||||
...createMEEnergyHazardLayers({ layers: layers as Record<string, boolean>, sc: zoomScale, fs: fontScale.facility, onPick: setMePickedFacility }),
|
||||
], [layers, zoomScale, fontScale.facility]);
|
||||
|
||||
useEffect(() => {
|
||||
if (flyToTarget && mapRef.current) {
|
||||
@ -234,7 +236,7 @@ export function SatelliteMap({ events, currentTime, aircraft, satellites, ships,
|
||||
layout={{
|
||||
'text-field': ['get', 'name'],
|
||||
'text-font': ['Open Sans Bold'],
|
||||
'text-size': 15,
|
||||
'text-size': 15 * fontScale.area,
|
||||
'text-allow-overlap': false,
|
||||
'text-ignore-placement': false,
|
||||
}}
|
||||
@ -251,7 +253,7 @@ export function SatelliteMap({ events, currentTime, aircraft, satellites, ships,
|
||||
layout={{
|
||||
'text-field': ['get', 'name'],
|
||||
'text-font': ['Open Sans Bold'],
|
||||
'text-size': 12,
|
||||
'text-size': 12 * fontScale.area,
|
||||
'text-allow-overlap': false,
|
||||
}}
|
||||
paint={{
|
||||
@ -268,7 +270,7 @@ export function SatelliteMap({ events, currentTime, aircraft, satellites, ships,
|
||||
layout={{
|
||||
'text-field': ['get', 'name'],
|
||||
'text-font': ['Open Sans Bold'],
|
||||
'text-size': 10,
|
||||
'text-size': 10 * fontScale.area,
|
||||
'text-allow-overlap': false,
|
||||
}}
|
||||
paint={{
|
||||
|
||||
@ -51,11 +51,13 @@ function hexToRgba(hex: string, alpha = 255): [number, number, number, number] {
|
||||
export interface IranAirportLayerConfig {
|
||||
visible: boolean;
|
||||
sc: number;
|
||||
fs?: number;
|
||||
onPick: (airport: Airport) => void;
|
||||
}
|
||||
|
||||
export function createIranAirportLayers(config: IranAirportLayerConfig): Layer[] {
|
||||
const { visible, sc, onPick } = config;
|
||||
const fs = config.fs ?? 1;
|
||||
if (!visible) return [];
|
||||
|
||||
const iconLayer = new IconLayer<Airport>({
|
||||
@ -84,7 +86,7 @@ export function createIranAirportLayers(config: IranAirportLayerConfig): Layer[]
|
||||
const nameKo = d.nameKo ?? d.name;
|
||||
return nameKo.length > 10 ? nameKo.slice(0, 10) + '..' : nameKo;
|
||||
},
|
||||
getSize: 11 * sc,
|
||||
getSize: 11 * sc * fs,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getColor: (d) => hexToRgba(getAirportColor(d)),
|
||||
getTextAnchor: 'middle',
|
||||
|
||||
@ -108,11 +108,13 @@ function hexToRgba(hex: string, alpha = 255): [number, number, number, number] {
|
||||
export interface IranOilLayerConfig {
|
||||
visible: boolean;
|
||||
sc: number;
|
||||
fs?: number;
|
||||
onPick: (facility: OilFacility) => void;
|
||||
}
|
||||
|
||||
export function createIranOilLayers(config: IranOilLayerConfig): Layer[] {
|
||||
const { visible, sc, onPick } = config;
|
||||
const fs = config.fs ?? 1;
|
||||
if (!visible) return [];
|
||||
|
||||
const iconLayer = new IconLayer<OilFacility>({
|
||||
@ -134,7 +136,7 @@ export function createIranOilLayers(config: IranOilLayerConfig): Layer[] {
|
||||
data: iranOilFacilities,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.nameKo.length > 10 ? d.nameKo.slice(0, 10) + '..' : d.nameKo,
|
||||
getSize: 12 * sc,
|
||||
getSize: 12 * sc * fs,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getColor: (d) => hexToRgba(TYPE_COLORS[d.type]),
|
||||
getTextAnchor: 'middle',
|
||||
|
||||
@ -103,11 +103,13 @@ function getIconUrl(type: MEFacilityType): string {
|
||||
export interface MEFacilityLayerConfig {
|
||||
visible: boolean;
|
||||
sc: number;
|
||||
fs?: number;
|
||||
onPick: (facility: MEFacility) => void;
|
||||
}
|
||||
|
||||
export function createMEFacilityLayers(config: MEFacilityLayerConfig): Layer[] {
|
||||
const { visible, sc, onPick } = config;
|
||||
const fs = config.fs ?? 1;
|
||||
if (!visible) return [];
|
||||
|
||||
const iconLayer = new IconLayer<MEFacility>({
|
||||
@ -129,7 +131,7 @@ export function createMEFacilityLayers(config: MEFacilityLayerConfig): Layer[] {
|
||||
data: ME_FACILITIES,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo,
|
||||
getSize: 12 * sc,
|
||||
getSize: 12 * sc * fs,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getColor: (d) => hexToRgba(ME_FACILITY_TYPE_META[d.type].color, 200),
|
||||
getTextAnchor: 'middle',
|
||||
|
||||
@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Map, NavigationControl, Marker, Source, Layer } from 'react-map-gl/maplibre';
|
||||
import type { MapRef } from 'react-map-gl/maplibre';
|
||||
import { ScatterplotLayer, TextLayer } from '@deck.gl/layers';
|
||||
import { useFontScale } from '../../hooks/useFontScale';
|
||||
import { DeckGLOverlay } from '../layers/DeckGLOverlay';
|
||||
import { useAnalysisDeckLayers } from '../../hooks/useAnalysisDeckLayers';
|
||||
import { useStaticDeckLayers } from '../../hooks/useStaticDeckLayers';
|
||||
@ -149,6 +150,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
const [trackCoords, setTrackCoords] = useState<[number, number][] | null>(null);
|
||||
const [selectedGearData, setSelectedGearData] = useState<SelectedGearGroupData | null>(null);
|
||||
const [selectedFleetData, setSelectedFleetData] = useState<SelectedFleetData | null>(null);
|
||||
const { fontScale } = useFontScale();
|
||||
const [zoomLevel, setZoomLevel] = useState(KOREA_MAP_ZOOM);
|
||||
const zoomRef = useRef(KOREA_MAP_ZOOM);
|
||||
const handleZoom = useCallback((e: { viewState: { zoom: number } }) => {
|
||||
@ -242,7 +244,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
data: illegalFishingData,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.name || d.mmsi,
|
||||
getSize: 11 * zoomScale,
|
||||
getSize: 11 * zoomScale * fontScale.analysis,
|
||||
getColor: [239, 68, 68, 255],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -253,8 +255,8 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
outlineColor: [0, 0, 0, 255],
|
||||
billboard: false,
|
||||
characterSet: 'auto',
|
||||
updateTriggers: { getSize: [zoomScale] },
|
||||
}), [illegalFishingData, zoomScale]);
|
||||
updateTriggers: { getSize: [zoomScale, fontScale.analysis] },
|
||||
}), [illegalFishingData, zoomScale, fontScale.analysis]);
|
||||
|
||||
// 수역 라벨 TextLayer — illegalFishing 또는 cnFishing 필터 활성 시 표시
|
||||
const zoneLabelsLayer = useMemo(() => {
|
||||
@ -281,7 +283,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
data,
|
||||
getPosition: (d: { lng: number; lat: number }) => [d.lng, d.lat],
|
||||
getText: (d: { name: string }) => d.name,
|
||||
getSize: 14 * zoomScale,
|
||||
getSize: 14 * zoomScale * fontScale.area,
|
||||
getColor: [255, 255, 255, 220],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'center',
|
||||
@ -292,9 +294,9 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
outlineColor: [0, 0, 0, 255],
|
||||
billboard: false,
|
||||
characterSet: 'auto',
|
||||
updateTriggers: { getSize: [zoomScale] },
|
||||
updateTriggers: { getSize: [zoomScale, fontScale.area] },
|
||||
});
|
||||
}, [koreaFilters.illegalFishing, koreaFilters.cnFishing, zoomScale]);
|
||||
}, [koreaFilters.illegalFishing, koreaFilters.cnFishing, zoomScale, fontScale.area]);
|
||||
|
||||
// 정적 레이어 (deck.gl) — 항구, 공항, 군사기지, 어항경고 등
|
||||
const staticDeckLayers = useStaticDeckLayers({
|
||||
@ -357,7 +359,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
data: gears,
|
||||
getPosition: (d: Ship) => [d.lng, d.lat],
|
||||
getText: (d: Ship) => d.name || d.mmsi,
|
||||
getSize: 10 * zoomScale,
|
||||
getSize: 10 * zoomScale * fontScale.analysis,
|
||||
getColor: [249, 115, 22, 255],
|
||||
getTextAnchor: 'middle' as const,
|
||||
getAlignmentBaseline: 'top' as const,
|
||||
@ -392,7 +394,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
data: [parent],
|
||||
getPosition: (d: Ship) => [d.lng, d.lat],
|
||||
getText: (d: Ship) => `▼ ${d.name || groupName} (모선)`,
|
||||
getSize: 11 * zoomScale,
|
||||
getSize: 11 * zoomScale * fontScale.analysis,
|
||||
getColor: [249, 115, 22, 255],
|
||||
getTextAnchor: 'middle' as const,
|
||||
getAlignmentBaseline: 'top' as const,
|
||||
@ -409,7 +411,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
}
|
||||
|
||||
return layers;
|
||||
}, [selectedGearData, zoomScale]);
|
||||
}, [selectedGearData, zoomScale, fontScale.analysis]);
|
||||
|
||||
// 선택된 선단 소속 선박 강조 레이어 (deck.gl)
|
||||
const selectedFleetLayers = useMemo(() => {
|
||||
@ -457,7 +459,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
const prefix = role === 'LEADER' ? '★ ' : '';
|
||||
return `${prefix}${d.name || d.mmsi}`;
|
||||
},
|
||||
getSize: 10 * zoomScale,
|
||||
getSize: 10 * zoomScale * fontScale.analysis,
|
||||
getColor: color,
|
||||
getTextAnchor: 'middle' as const,
|
||||
getAlignmentBaseline: 'top' as const,
|
||||
@ -495,7 +497,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [selectedFleetData, zoomScale, vesselAnalysis]);
|
||||
}, [selectedFleetData, zoomScale, vesselAnalysis, fontScale.analysis]);
|
||||
|
||||
// 분석 결과 deck.gl 레이어
|
||||
const analysisActiveFilter = koreaFilters.illegalFishing ? 'illegalFishing'
|
||||
@ -527,7 +529,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
filter={['==', ['get', 'rank'], 1]}
|
||||
layout={{
|
||||
'text-field': ['get', 'name'],
|
||||
'text-size': 15,
|
||||
'text-size': 15 * fontScale.area,
|
||||
'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'],
|
||||
'text-allow-overlap': false,
|
||||
'text-ignore-placement': false,
|
||||
@ -546,7 +548,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
filter={['==', ['get', 'rank'], 2]}
|
||||
layout={{
|
||||
'text-field': ['get', 'name'],
|
||||
'text-size': 12,
|
||||
'text-size': 12 * fontScale.area,
|
||||
'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'],
|
||||
'text-allow-overlap': false,
|
||||
'text-ignore-placement': false,
|
||||
@ -566,7 +568,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
||||
minzoom={5}
|
||||
layout={{
|
||||
'text-field': ['get', 'name'],
|
||||
'text-size': 10,
|
||||
'text-size': 10 * fontScale.area,
|
||||
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
|
||||
'text-allow-overlap': false,
|
||||
'text-ignore-placement': false,
|
||||
|
||||
@ -6,6 +6,7 @@ import maplibregl from 'maplibre-gl';
|
||||
import { MT_TYPE_COLORS, MT_TYPE_HEX, getMTType, NAVY_COLORS, FLAG_EMOJI, SIZE_MAP, isMilitary, getShipColor } from '../../utils/shipClassification';
|
||||
import { getMarineTrafficCategory } from '../../utils/marineTraffic';
|
||||
import { getNationalityGroup } from '../../hooks/useKoreaData';
|
||||
import { useFontScale } from '../../hooks/useFontScale';
|
||||
|
||||
interface Props {
|
||||
ships: Ship[];
|
||||
@ -274,6 +275,8 @@ function ensureTriangleImage(map: maplibregl.Map) {
|
||||
// ── Main layer (WebGL symbol rendering — triangles) ──
|
||||
export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusMmsi, onFocusClear, analysisMap, hiddenShipCategories, hiddenNationalities }: Props) {
|
||||
const { current: map } = useMap();
|
||||
const { fontScale } = useFontScale();
|
||||
const sfs = fontScale.ship;
|
||||
const [selectedMmsi, setSelectedMmsi] = useState<string | null>(null);
|
||||
const [imageReady, setImageReady] = useState(false);
|
||||
const highlightKorean = !!koreanOnly;
|
||||
@ -479,13 +482,14 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM
|
||||
/>
|
||||
{/* Korean ship label — always mounted, visibility으로 제어 */}
|
||||
<Layer
|
||||
id="ships-korean-label"
|
||||
key={`ships-korean-label-${sfs}`}
|
||||
id={`ships-korean-label-${sfs}`}
|
||||
type="symbol"
|
||||
filter={['==', ['get', 'isKorean'], 1]}
|
||||
layout={{
|
||||
'visibility': highlightKorean ? 'visible' : 'none',
|
||||
'text-field': ['get', 'name'],
|
||||
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 6, 6, 7, 8, 9, 10, 11, 12, 13, 13, 15, 14, 17],
|
||||
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 6*sfs, 6, 7*sfs, 8, 9*sfs, 10, 11*sfs, 12, 13*sfs, 13, 15*sfs, 14, 17*sfs],
|
||||
'text-offset': [0, 2.2],
|
||||
'text-anchor': 'top',
|
||||
'text-allow-overlap': false,
|
||||
|
||||
10
frontend/src/contexts/FontScaleContext.tsx
Normal file
10
frontend/src/contexts/FontScaleContext.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
import { FontScaleCtx, DEFAULT_FONT_SCALE } from './fontScaleState';
|
||||
|
||||
export type { FontScaleConfig } from './fontScaleState';
|
||||
|
||||
export function FontScaleProvider({ children }: { children: ReactNode }) {
|
||||
const [fontScale, setFontScale] = useLocalStorage('mapFontScale', DEFAULT_FONT_SCALE);
|
||||
return <FontScaleCtx.Provider value={{ fontScale, setFontScale }}>{children}</FontScaleCtx.Provider>;
|
||||
}
|
||||
14
frontend/src/contexts/fontScaleState.ts
Normal file
14
frontend/src/contexts/fontScaleState.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface FontScaleConfig {
|
||||
facility: number;
|
||||
ship: number;
|
||||
analysis: number;
|
||||
area: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_FONT_SCALE: FontScaleConfig = { facility: 1.0, ship: 1.0, analysis: 1.0, area: 1.0 };
|
||||
|
||||
export const FontScaleCtx = createContext<{ fontScale: FontScaleConfig; setFontScale: (c: FontScaleConfig) => void }>({
|
||||
fontScale: DEFAULT_FONT_SCALE, setFontScale: () => {},
|
||||
});
|
||||
@ -353,8 +353,8 @@ export function createFacilityLayers(
|
||||
data: plants,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => (d.name.length > 10 ? d.name.slice(0, 10) + '..' : d.name),
|
||||
getSize: 12 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 12 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => [...hexToRgb(infraColor(d)), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -409,8 +409,8 @@ export function createFacilityLayers(
|
||||
data: hazardData,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo,
|
||||
getSize: 12 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 12 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => HAZARD_META[d.type]?.color ?? [200, 200, 200, 200],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -463,8 +463,8 @@ export function createFacilityLayers(
|
||||
data: cnData,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.name,
|
||||
getSize: 12 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 12 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => CN_META[d.subType]?.color ?? [200, 200, 200, 200],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -516,8 +516,8 @@ export function createFacilityLayers(
|
||||
data: jpData,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.name,
|
||||
getSize: 12 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 12 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => JP_META[d.subType]?.color ?? [200, 200, 200, 200],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
|
||||
@ -312,8 +312,8 @@ export function createMilitaryLayers(
|
||||
data: MILITARY_BASES,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => (d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo),
|
||||
getSize: 11 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 11 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => [...hexToRgb(MIL_BASE_TYPE_COLOR[d.type] ?? '#a78bfa'), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -350,8 +350,8 @@ export function createMilitaryLayers(
|
||||
data: GOV_BUILDINGS,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => (d.nameKo.length > 10 ? d.nameKo.slice(0, 10) + '..' : d.nameKo),
|
||||
getSize: 11 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 11 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => [...hexToRgb(GOV_TYPE_COLOR[d.type] ?? '#f59e0b'), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -388,8 +388,8 @@ export function createMilitaryLayers(
|
||||
data: NK_LAUNCH_SITES,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => (d.nameKo.length > 10 ? d.nameKo.slice(0, 10) + '..' : d.nameKo),
|
||||
getSize: 11 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 11 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => [...hexToRgb(NK_LAUNCH_TYPE_META[d.type]?.color ?? '#f97316'), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -480,8 +480,8 @@ export function createMilitaryLayers(
|
||||
data: impactData,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => `${d.ev.date.slice(5)} ${d.ev.time} ← ${d.ev.launchNameKo}`,
|
||||
getSize: 11 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 11 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => [...hexToRgb(getMissileColor(d.ev.type)), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
|
||||
@ -174,8 +174,8 @@ export function createNavigationLayers(
|
||||
if (d.type === 'navy') return d.name.replace('해군', '').replace('사령부', '사').replace('전대', '전').replace('전단', '전').trim().slice(0, 8);
|
||||
return d.name.replace('해양경찰청', '').replace('지방', '').trim() || '본청';
|
||||
},
|
||||
getSize: 12 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 12 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => [...hexToRgb(CG_TYPE_COLOR[d.type]), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -225,8 +225,8 @@ export function createNavigationLayers(
|
||||
data: KOREAN_AIRPORTS,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.nameKo.replace('국제공항', '').replace('공항', ''),
|
||||
getSize: 12 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 12 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => [...hexToRgb(apColor(d)), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -275,8 +275,8 @@ export function createNavigationLayers(
|
||||
data: NAV_WARNINGS,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.id,
|
||||
getSize: 12 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 12 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => [...hexToRgb(NW_ORG_COLOR[d.org]), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -326,8 +326,8 @@ export function createNavigationLayers(
|
||||
data: PIRACY_ZONES,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.nameKo,
|
||||
getSize: 12 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 12 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => [...hexToRgb(PIRACY_LEVEL_COLOR[d.level] ?? '#ef4444'), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
|
||||
@ -95,8 +95,8 @@ export function createPortLayers(
|
||||
data: EAST_ASIA_PORTS,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => d.nameKo.replace('항', ''),
|
||||
getSize: 12 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 12 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: (d) => [...hexToRgb(PORT_COUNTRY_COLOR[d.country] ?? PORT_COUNTRY_COLOR.KR), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
@ -133,8 +133,8 @@ export function createPortLayers(
|
||||
data: KOREA_WIND_FARMS,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getText: (d) => (d.name.length > 10 ? d.name.slice(0, 10) + '..' : d.name),
|
||||
getSize: 12 * sc,
|
||||
updateTriggers: { getSize: [sc] },
|
||||
getSize: 12 * sc * fc.fs,
|
||||
updateTriggers: { getSize: [sc, fc.fs] },
|
||||
getColor: [...hexToRgb(WIND_COLOR), 255] as [number, number, number, number],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'top',
|
||||
|
||||
@ -32,7 +32,8 @@ export interface StaticPickInfo {
|
||||
}
|
||||
|
||||
export interface LayerFactoryConfig {
|
||||
sc: number; // sizeScale
|
||||
sc: number; // sizeScale (zoom-based)
|
||||
fs: number; // fontScale (user preference, default 1.0)
|
||||
onPick: (info: StaticPickInfo) => void;
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
import { ScatterplotLayer, TextLayer } from '@deck.gl/layers';
|
||||
import type { Layer } from '@deck.gl/core';
|
||||
import type { Ship, VesselAnalysisDto } from '../types';
|
||||
import { useFontScale } from './useFontScale';
|
||||
|
||||
interface AnalyzedShip {
|
||||
ship: Ship;
|
||||
@ -57,6 +58,8 @@ export function useAnalysisDeckLayers(
|
||||
activeFilter: string | null,
|
||||
sizeScale: number = 1.0,
|
||||
): Layer[] {
|
||||
const { fontScale } = useFontScale();
|
||||
const afs = fontScale.analysis;
|
||||
// 데이터 준비: ships 필터/정렬/슬라이스 — sizeScale 변경 시 재실행 안 됨
|
||||
const { riskData, darkData, spoofData } = useMemo<AnalysisData>(() => {
|
||||
if (analysisMap.size === 0) {
|
||||
@ -123,7 +126,7 @@ export function useAnalysisDeckLayers(
|
||||
const name = d.ship.name || d.ship.mmsi;
|
||||
return `${name}\n${label} ${d.dto.algorithms.riskScore.score}`;
|
||||
},
|
||||
getSize: 10 * sizeScale,
|
||||
getSize: 10 * sizeScale * afs,
|
||||
updateTriggers: { getSize: [sizeScale] },
|
||||
getColor: (d) => RISK_RGBA_BORDER[d.dto.algorithms.riskScore.level] ?? [200, 200, 200, 255],
|
||||
getTextAnchor: 'middle',
|
||||
@ -167,7 +170,7 @@ export function useAnalysisDeckLayers(
|
||||
const gap = d.dto.algorithms.darkVessel.gapDurationMin;
|
||||
return gap > 0 ? `AIS 소실 ${Math.round(gap)}분` : 'DARK';
|
||||
},
|
||||
getSize: 10 * sizeScale,
|
||||
getSize: 10 * sizeScale * afs,
|
||||
updateTriggers: { getSize: [sizeScale] },
|
||||
getColor: [168, 85, 247, 255],
|
||||
getTextAnchor: 'middle',
|
||||
@ -191,7 +194,7 @@ export function useAnalysisDeckLayers(
|
||||
data: spoofData,
|
||||
getPosition: (d) => [d.ship.lng, d.ship.lat],
|
||||
getText: (d) => `GPS ${Math.round(d.dto.algorithms.gpsSpoofing.spoofingScore * 100)}%`,
|
||||
getSize: 10 * sizeScale,
|
||||
getSize: 10 * sizeScale * afs,
|
||||
getColor: [239, 68, 68, 255],
|
||||
getTextAnchor: 'start',
|
||||
getPixelOffset: [12, -8],
|
||||
@ -207,5 +210,5 @@ export function useAnalysisDeckLayers(
|
||||
}
|
||||
|
||||
return layers;
|
||||
}, [riskData, darkData, spoofData, sizeScale, activeFilter]);
|
||||
}, [riskData, darkData, spoofData, sizeScale, activeFilter, afs]);
|
||||
}
|
||||
|
||||
4
frontend/src/hooks/useFontScale.ts
Normal file
4
frontend/src/hooks/useFontScale.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { useContext } from 'react';
|
||||
import { FontScaleCtx } from '../contexts/fontScaleState';
|
||||
|
||||
export function useFontScale() { return useContext(FontScaleCtx); }
|
||||
@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
import type { Layer } from '@deck.gl/core';
|
||||
import type { PowerFacility } from '../services/infra';
|
||||
import type { HazardType } from '../data/hazardFacilities';
|
||||
import { useFontScale } from './useFontScale';
|
||||
|
||||
// Re-export types for consumers
|
||||
export type { StaticPickedObject, StaticLayerKind, StaticPickInfo } from './layers/types';
|
||||
@ -34,8 +35,9 @@ interface StaticLayerConfig {
|
||||
}
|
||||
|
||||
export function useStaticDeckLayers(config: StaticLayerConfig): Layer[] {
|
||||
const { fontScale } = useFontScale();
|
||||
return useMemo(() => {
|
||||
const fc = { sc: config.sizeScale ?? 1.0, onPick: config.onPick };
|
||||
const fc = { sc: config.sizeScale ?? 1.0, fs: fontScale.facility, onPick: config.onPick };
|
||||
|
||||
return [
|
||||
...createPortLayers({ ports: config.ports, windFarm: config.windFarm }, fc),
|
||||
@ -81,5 +83,6 @@ export function useStaticDeckLayers(config: StaticLayerConfig): Layer[] {
|
||||
config.jpMilitary,
|
||||
config.onPick,
|
||||
config.sizeScale,
|
||||
fontScale.facility,
|
||||
]);
|
||||
}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user