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>
194 lines
5.9 KiB
TypeScript
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]);
|
|
}
|