import { useEffect, useRef, type MutableRefObject } from 'react'; import maplibregl from 'maplibre-gl'; import type { MapStyleSettings, MapLabelLanguage, DepthColorStop, DepthFontSize } from '../../../features/mapSettings/types'; import type { BaseMapId } from '../types'; import { kickRepaint, onMapStyleReady } from '../lib/mapCore'; /* ── Depth font size presets ──────────────────────────────────────── */ const DEPTH_FONT_SIZE_MAP: Record = { small: ['interpolate', ['linear'], ['zoom'], 7, 8, 9, 9, 11, 11, 13, 13], medium: ['interpolate', ['linear'], ['zoom'], 7, 10, 9, 12, 11, 14, 13, 16], large: ['interpolate', ['linear'], ['zoom'], 7, 12, 9, 15, 11, 18, 13, 20], }; /* ── Helpers ──────────────────────────────────────────────────────── */ function darkenHex(hex: string, factor = 0.85): string { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `#${[r, g, b].map((c) => Math.round(c * factor).toString(16).padStart(2, '0')).join('')}`; } function lightenHex(hex: string, factor = 1.3): string { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `#${[r, g, b].map((c) => Math.min(255, Math.round(c * factor)).toString(16).padStart(2, '0')).join('')}`; } /* ── Apply functions ──────────────────────────────────────────────── */ function applyLabelLanguage(map: maplibregl.Map, lang: MapLabelLanguage) { const style = map.getStyle(); if (!style?.layers) return; for (const layer of style.layers) { if (layer.type !== 'symbol') continue; const layout = (layer as { layout?: Record }).layout; if (!layout?.['text-field']) continue; if (layer.id.startsWith('bathymetry-labels')) continue; const textField = lang === 'local' ? ['get', 'name'] : ['coalesce', ['get', `name:${lang}`], ['get', 'name']]; try { map.setLayoutProperty(layer.id, 'text-field', textField); } catch { // ignore } } } function applyLandColor(map: maplibregl.Map, color: string) { const style = map.getStyle(); if (!style?.layers) return; const waterRegex = /(water|sea|ocean|river|lake|coast|bay)/i; const darkVariant = darkenHex(color, 0.8); for (const layer of style.layers) { const id = layer.id; if (id.startsWith('bathymetry-')) continue; if (id.startsWith('subcables-')) continue; if (id.startsWith('zones-')) continue; if (id.startsWith('ships-')) continue; if (id.startsWith('pair-')) continue; if (id.startsWith('fc-')) continue; if (id.startsWith('fleet-')) continue; if (id.startsWith('predict-')) continue; if (id.startsWith('vessel-track-')) continue; if (id === 'deck-globe') continue; const sourceLayer = String((layer as Record)['source-layer'] ?? ''); const isWater = waterRegex.test(id) || waterRegex.test(sourceLayer); if (isWater) continue; try { if (layer.type === 'background') { map.setPaintProperty(id, 'background-color', color); } else if (layer.type === 'fill') { map.setPaintProperty(id, 'fill-color', darkVariant); } } catch { // ignore } } } function applyWaterBaseColor(map: maplibregl.Map, fillColor: string) { const style = map.getStyle(); if (!style?.layers) return; const waterRegex = /(water|sea|ocean|river|lake|coast|bay)/i; const lineColor = darkenHex(fillColor, 0.85); for (const layer of style.layers) { const id = layer.id; if (id.startsWith('bathymetry-')) continue; const sourceLayer = String((layer as Record)['source-layer'] ?? ''); if (!waterRegex.test(id) && !waterRegex.test(sourceLayer)) continue; try { if (layer.type === 'fill') { map.setPaintProperty(id, 'fill-color', fillColor); } else if (layer.type === 'line') { map.setPaintProperty(id, 'line-color', lineColor); } } catch { // ignore } } } function applyDepthGradient(map: maplibregl.Map, stops: DepthColorStop[]) { const depth = ['to-number', ['get', 'depth']]; const sorted = [...stops].sort((a, b) => a.depth - b.depth); if (sorted.length === 0) return; const expr: unknown[] = ['interpolate', ['linear'], depth]; for (const s of sorted) { expr.push(s.depth, s.color); } // 0m까지 확장 (최천층 stop이 0보다 깊으면) const shallowest = sorted[sorted.length - 1]; if (shallowest.depth < 0) { expr.push(0, lightenHex(shallowest.color, 1.8)); } if (!map.getLayer('bathymetry-fill')) return; try { map.setPaintProperty('bathymetry-fill', 'fill-color', expr as never); } catch { // ignore } } function applyDepthFontSize(map: maplibregl.Map, size: DepthFontSize) { const expr = DEPTH_FONT_SIZE_MAP[size]; for (const layerId of ['bathymetry-labels', 'bathymetry-labels-coarse', 'bathymetry-landforms']) { if (!map.getLayer(layerId)) continue; try { map.setLayoutProperty(layerId, 'text-size', expr); } catch { // ignore } } } function applyDepthFontColor(map: maplibregl.Map, color: string) { for (const layerId of ['bathymetry-labels', 'bathymetry-labels-coarse', 'bathymetry-landforms']) { if (!map.getLayer(layerId)) continue; try { map.setPaintProperty(layerId, 'text-color', color); } catch { // ignore } } } /* ── Hook ──────────────────────────────────────────────────────────── */ export function useMapStyleSettings( mapRef: MutableRefObject, settings: MapStyleSettings | undefined, opts: { baseMap: BaseMapId; mapSyncEpoch: number }, ) { const settingsRef = useRef(settings); useEffect(() => { settingsRef.current = settings; }); const { baseMap, mapSyncEpoch } = opts; useEffect(() => { const map = mapRef.current; const s = settingsRef.current; if (!map || !s) return; const stop = onMapStyleReady(map, () => { applyLabelLanguage(map, s.labelLanguage); applyLandColor(map, s.landColor); applyWaterBaseColor(map, s.waterBaseColor); if (baseMap === 'enhanced') { applyDepthGradient(map, s.depthStops); applyDepthFontSize(map, s.depthFontSize); applyDepthFontColor(map, s.depthFontColor); } kickRepaint(map); }); return () => stop(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [settings, baseMap, mapSyncEpoch]); }