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 === '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 landRegex = /(land|landcover|landuse|earth|continent|terrain|park)/i; for (const layer of style.layers) { if (layer.type !== 'fill') continue; const id = layer.id; const sourceLayer = String((layer as Record)['source-layer'] ?? ''); if (!landRegex.test(id) && !landRegex.test(sourceLayer)) continue; try { map.setPaintProperty(id, 'fill-color', color); } 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[]) { if (!map.getLayer('bathymetry-fill')) return; const depth = ['to-number', ['get', 'depth']]; const sorted = [...stops].sort((a, b) => a.depth - b.depth); const expr: unknown[] = ['interpolate', ['linear'], depth]; const deepest = sorted[0]; if (deepest) expr.push(-11000, darkenHex(deepest.color, 0.5)); for (const s of sorted) { expr.push(s.depth, s.color); } const shallowest = sorted[sorted.length - 1]; if (shallowest) expr.push(0, lightenHex(shallowest.color, 1.8)); 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-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-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]); }