gc-wing/apps/web/src/features/oceanMap/hooks/useOceanMapSettings.ts
htlee 12fdae9a2e feat(ocean-map): Ocean 전용 지도 모듈 추가
MapTiler Ocean 완전 스타일 기반 별도 베이스맵 모드.
features/oceanMap/ 자체 완결 블록 — 기존 enhanced 코드 변경 없음.

- resolveOceanStyle: Ocean style.json fetch + 한국어 라벨
- useOceanMapSettings: 런타임 커스텀 (수심색상/등심선/hillshade/라벨)
- OceanMapSettingsPanel: 9개 섹션 설정 UI
- 사이드바 Ocean 토글 + 설정 패널 baseMap 분기
- resolveMapStyle dynamic import로 번들 분리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 23:14:48 +09:00

194 lines
5.9 KiB
TypeScript

import { useEffect, useRef, type MutableRefObject } from 'react';
import maplibregl from 'maplibre-gl';
import type { OceanMapSettings, OceanDepthStop, OceanDepthLabelSize, OceanLabelLanguage } from '../model/types';
import type { BaseMapId } from '../../../widgets/map3d/types';
import { kickRepaint, onMapStyleReady } from '../../../widgets/map3d/lib/mapCore';
import { discoverOceanLayers } from '../lib/oceanLayerIds';
/* ── Depth font size presets ──────────────────────────────────────── */
const OCEAN_DEPTH_FONT_SIZE: Record<OceanDepthLabelSize, unknown[]> = {
small: ['interpolate', ['linear'], ['zoom'], 5, 8, 8, 10, 11, 12],
medium: ['interpolate', ['linear'], ['zoom'], 5, 10, 8, 12, 11, 15],
large: ['interpolate', ['linear'], ['zoom'], 5, 12, 8, 15, 11, 18],
};
/* ── Apply functions (Ocean 전용 — enhanced 코드와 공유 없음) ────── */
function applyOceanDepthColors(map: maplibregl.Map, layers: string[], stops: OceanDepthStop[], opacity: number) {
if (layers.length === 0) return;
const depth = ['to-number', ['get', 'depth']];
const sorted = [...stops].sort((a, b) => a.depth - b.depth);
if (sorted.length < 2) return;
const expr: unknown[] = ['interpolate', ['linear'], depth];
for (const s of sorted) {
expr.push(s.depth, s.color);
}
for (const id of layers) {
if (!map.getLayer(id)) continue;
try {
map.setPaintProperty(id, 'fill-color', expr as never);
map.setPaintProperty(id, 'fill-opacity', opacity);
} catch {
// ignore
}
}
}
function applyOceanContourStyle(
map: maplibregl.Map,
layers: string[],
visible: boolean,
color: string,
opacity: number,
width: number,
) {
for (const id of layers) {
if (!map.getLayer(id)) continue;
try {
map.setLayoutProperty(id, 'visibility', visible ? 'visible' : 'none');
if (visible) {
map.setPaintProperty(id, 'line-color', color);
map.setPaintProperty(id, 'line-opacity', opacity);
map.setPaintProperty(id, 'line-width', width);
}
} catch {
// ignore
}
}
}
function applyOceanDepthLabels(
map: maplibregl.Map,
layers: string[],
visible: boolean,
color: string,
size: OceanDepthLabelSize,
) {
const sizeExpr = OCEAN_DEPTH_FONT_SIZE[size];
for (const id of layers) {
if (!map.getLayer(id)) continue;
try {
map.setLayoutProperty(id, 'visibility', visible ? 'visible' : 'none');
if (visible) {
map.setPaintProperty(id, 'text-color', color);
map.setLayoutProperty(id, 'text-size', sizeExpr);
}
} catch {
// ignore
}
}
}
function applyOceanHillshade(
map: maplibregl.Map,
layers: string[],
visible: boolean,
exaggeration: number,
color: string,
) {
for (const id of layers) {
if (!map.getLayer(id)) continue;
try {
map.setLayoutProperty(id, 'visibility', visible ? 'visible' : 'none');
if (visible) {
map.setPaintProperty(id, 'hillshade-exaggeration', exaggeration);
map.setPaintProperty(id, 'hillshade-shadow-color', color);
}
} catch {
// ignore
}
}
}
function applyOceanLandformLabels(
map: maplibregl.Map,
layers: string[],
visible: boolean,
color: string,
) {
for (const id of layers) {
if (!map.getLayer(id)) continue;
try {
map.setLayoutProperty(id, 'visibility', visible ? 'visible' : 'none');
if (visible) {
map.setPaintProperty(id, 'text-color', color);
}
} catch {
// ignore
}
}
}
function applyOceanBackground(map: maplibregl.Map, layers: string[], color: string) {
for (const id of layers) {
if (!map.getLayer(id)) continue;
try {
map.setPaintProperty(id, 'background-color', color);
} catch {
// ignore
}
}
try {
map.getCanvas().style.background = color;
} catch {
// ignore
}
}
function applyOceanLabelLanguage(map: maplibregl.Map, layers: string[], lang: OceanLabelLanguage) {
const textField =
lang === 'local'
? ['get', 'name']
: ['coalesce', ['get', `name:${lang}`], ['get', 'name']];
for (const id of layers) {
if (!map.getLayer(id)) continue;
try {
map.setLayoutProperty(id, 'text-field', textField);
} catch {
// ignore
}
}
}
/* ── Hook ──────────────────────────────────────────────────────────── */
export function useOceanMapSettings(
mapRef: MutableRefObject<maplibregl.Map | null>,
settings: OceanMapSettings | undefined,
opts: { baseMap: BaseMapId; mapSyncEpoch: number },
) {
const settingsRef = useRef(settings);
useEffect(() => {
settingsRef.current = settings;
});
const { baseMap, mapSyncEpoch } = opts;
useEffect(() => {
// Ocean 전용 — enhanced 모드에서는 즉시 return
if (baseMap !== 'ocean') return;
const map = mapRef.current;
const s = settingsRef.current;
if (!map || !s) return;
const stop = onMapStyleReady(map, () => {
const oceanLayers = discoverOceanLayers(map);
applyOceanDepthColors(map, oceanLayers.depthFill, s.depthStops, s.depthOpacity);
applyOceanContourStyle(map, oceanLayers.contourLine, s.contourVisible, s.contourColor, s.contourOpacity, s.contourWidth);
applyOceanDepthLabels(map, oceanLayers.depthLabel, s.depthLabelsVisible, s.depthLabelColor, s.depthLabelSize);
applyOceanHillshade(map, oceanLayers.hillshade, s.hillshadeVisible, s.hillshadeExaggeration, s.hillshadeColor);
applyOceanLandformLabels(map, oceanLayers.landformLabel, s.landformLabelsVisible, s.landformLabelColor);
applyOceanBackground(map, oceanLayers.background, s.backgroundColor);
applyOceanLabelLanguage(map, oceanLayers.allSymbol, s.labelLanguage);
kickRepaint(map);
});
return () => stop();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings, baseMap, mapSyncEpoch]);
}