import { useEffect, type MutableRefObject } from 'react'; import type maplibregl from 'maplibre-gl'; import type { GeoJSONSource, GeoJSONSourceSpecification, LayerSpecification } from 'maplibre-gl'; import type { AisTarget } from '../../../entities/aisTarget/model/types'; import type { LegacyVesselInfo } from '../../../entities/legacyVessel/model/types'; import type { MapToggleState } from '../../../features/mapToggles/MapToggles'; import type { Map3DSettings, MapProjectionId } from '../types'; import { kickRepaint, onMapStyleReady } from '../lib/mapCore'; /** Mercator 모드 선명 라벨 (허가 선박 + 선택/하이라이트) */ export function useGlobeShipLabels( mapRef: MutableRefObject, projectionBusyRef: MutableRefObject, opts: { projection: MapProjectionId; settings: Map3DSettings; shipData: AisTarget[]; shipHighlightSet: Set; overlays: MapToggleState; legacyHits: Map | null | undefined; selectedMmsi: number | null; mapSyncEpoch: number; }, ) { const { projection, settings, shipData, shipHighlightSet, overlays, legacyHits, selectedMmsi, mapSyncEpoch, } = opts; useEffect(() => { const map = mapRef.current; if (!map) return; const srcId = 'ship-labels-src'; const layerId = 'ship-labels'; const remove = () => { try { if (map.getLayer(layerId)) map.removeLayer(layerId); } catch { // ignore } try { if (map.getSource(srcId)) map.removeSource(srcId); } catch { // ignore } }; const ensure = () => { if (projectionBusyRef.current) return; if (!map.isStyleLoaded()) return; if (projection !== 'mercator' || !settings.showShips) { remove(); return; } const visibility = overlays.shipLabels ? 'visible' : 'none'; const features: GeoJSON.Feature[] = []; for (const t of shipData) { const legacy = legacyHits?.get(t.mmsi) ?? null; const isTarget = !!legacy; const isSelected = selectedMmsi != null && t.mmsi === selectedMmsi; const isPinnedHighlight = shipHighlightSet.has(t.mmsi); if (!isTarget && !isSelected && !isPinnedHighlight) continue; const labelName = (legacy?.shipNameCn || legacy?.shipNameRoman || t.name || '').trim(); if (!labelName) continue; features.push({ type: 'Feature', id: `ship-label-${t.mmsi}`, geometry: { type: 'Point', coordinates: [t.lon, t.lat] }, properties: { mmsi: t.mmsi, labelName, selected: isSelected ? 1 : 0, highlighted: isPinnedHighlight ? 1 : 0, permitted: isTarget ? 1 : 0, }, }); } const fc: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features }; try { const existing = map.getSource(srcId) as GeoJSONSource | undefined; if (existing) existing.setData(fc); else map.addSource(srcId, { type: 'geojson', data: fc } as GeoJSONSourceSpecification); } catch (e) { console.warn('Ship label source setup failed:', e); return; } const filter = ['!=', ['to-string', ['coalesce', ['get', 'labelName'], '']], ''] as unknown as unknown[]; if (!map.getLayer(layerId)) { try { map.addLayer( { id: layerId, type: 'symbol', source: srcId, minzoom: 7, filter: filter as never, layout: { visibility, 'symbol-placement': 'point', 'text-field': ['get', 'labelName'] as never, 'text-font': ['Noto Sans Regular', 'Open Sans Regular'], 'text-size': ['interpolate', ['linear'], ['zoom'], 7, 10, 10, 11, 12, 12, 14, 13] as never, 'text-anchor': 'top', 'text-offset': [0, 1.1], 'text-padding': 2, 'text-allow-overlap': false, 'text-ignore-placement': false, }, paint: { 'text-color': [ 'case', ['==', ['get', 'selected'], 1], 'rgba(14,234,255,0.95)', ['==', ['get', 'highlighted'], 1], 'rgba(245,158,11,0.95)', 'rgba(226,232,240,0.92)', ] as never, 'text-halo-color': 'rgba(2,6,23,0.85)', 'text-halo-width': 1.2, 'text-halo-blur': 0.8, }, } as unknown as LayerSpecification, undefined, ); } catch (e) { console.warn('Ship label layer add failed:', e); } } else { try { map.setLayoutProperty(layerId, 'visibility', visibility); } catch { // ignore } } kickRepaint(map); }; const stop = onMapStyleReady(map, ensure); return () => { stop(); }; }, [ projection, settings.showShips, overlays.shipLabels, shipData, legacyHits, selectedMmsi, shipHighlightSet, mapSyncEpoch, ]); }