gc-wing/apps/web/src/widgets/map3d/hooks/useGlobeShipLabels.ts
htlee e2dc927ad2 refactor(map3d): useGlobeShips 977줄 → 서브훅 3+1개 분리
- useGlobeShipLabels: Mercator 선명 라벨
- useGlobeShipLayers: Globe 선박 아이콘 레이어 + GeoJSON
- useGlobeShipHover: Globe 호버 오버레이 + 클릭 선택
- useGlobeShips: 오케스트레이터 (기존 호출부 호환)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:35:03 +09:00

165 lines
5.2 KiB
TypeScript

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<maplibregl.Map | null>,
projectionBusyRef: MutableRefObject<boolean>,
opts: {
projection: MapProjectionId;
settings: Map3DSettings;
shipData: AisTarget[];
shipHighlightSet: Set<number>;
overlays: MapToggleState;
legacyHits: Map<number, LegacyVesselInfo> | 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<GeoJSON.Point>[] = [];
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<GeoJSON.Point> = { 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,
]);
}