import { useEffect, type MutableRefObject } from 'react'; import maplibregl, { type GeoJSONSource, type GeoJSONSourceSpecification, type LayerSpecification, } from 'maplibre-gl'; import type { ZoneId } from '../../../entities/zone/model/meta'; import { ZONE_META } from '../../../entities/zone/model/meta'; import type { ZonesGeoJson } from '../../../entities/zone/api/useZones'; import type { MapToggleState } from '../../../features/mapToggles/MapToggles'; import type { BaseMapId, MapProjectionId } from '../types'; import { kickRepaint, onMapStyleReady } from '../lib/mapCore'; export function useZonesLayer( mapRef: MutableRefObject, projectionBusyRef: MutableRefObject, reorderGlobeFeatureLayers: () => void, opts: { zones: ZonesGeoJson | null; overlays: MapToggleState; projection: MapProjectionId; baseMap: BaseMapId; hoveredZoneId: string | null; mapSyncEpoch: number; }, ) { const { zones, overlays, projection, baseMap, hoveredZoneId, mapSyncEpoch } = opts; useEffect(() => { const map = mapRef.current; if (!map) return; const srcId = 'zones-src'; const fillId = 'zones-fill'; const lineId = 'zones-line'; const labelId = 'zones-label'; const zoneColorExpr: unknown[] = ['match', ['get', 'zoneId']]; for (const k of Object.keys(ZONE_META) as ZoneId[]) { zoneColorExpr.push(k, ZONE_META[k].color); } zoneColorExpr.push('#3B82F6'); const zoneLabelExpr: unknown[] = ['match', ['to-string', ['coalesce', ['get', 'zoneId'], '']]]; for (const k of Object.keys(ZONE_META) as ZoneId[]) { zoneLabelExpr.push(k, ZONE_META[k].name); } zoneLabelExpr.push(['coalesce', ['get', 'zoneName'], ['get', 'zoneLabel'], ['get', 'NAME'], '수역']); const ensure = () => { if (projectionBusyRef.current) return; const visibility = overlays.zones ? 'visible' : 'none'; try { if (map.getLayer(fillId)) map.setLayoutProperty(fillId, 'visibility', visibility); } catch { // ignore } try { if (map.getLayer(lineId)) map.setLayoutProperty(lineId, 'visibility', visibility); } catch { // ignore } try { if (map.getLayer(labelId)) map.setLayoutProperty(labelId, 'visibility', visibility); } catch { // ignore } if (!zones) return; if (!map.isStyleLoaded()) return; try { const existing = map.getSource(srcId) as GeoJSONSource | undefined; if (existing) { existing.setData(zones); } else { map.addSource(srcId, { type: 'geojson', data: zones } as GeoJSONSourceSpecification); } const style = map.getStyle(); const styleLayers = style && Array.isArray(style.layers) ? style.layers : []; const firstSymbol = styleLayers.find((l) => (l as { type?: string } | undefined)?.type === 'symbol') as | { id?: string } | undefined; const before = map.getLayer('deck-globe') ? 'deck-globe' : map.getLayer('ships') ? 'ships' : map.getLayer('seamark') ? 'seamark' : firstSymbol?.id; const zoneMatchExpr = hoveredZoneId !== null ? (['==', ['to-string', ['coalesce', ['get', 'zoneId'], '']], hoveredZoneId] as unknown[]) : false; const zoneLineWidthExpr = hoveredZoneId ? ([ 'interpolate', ['linear'], ['zoom'], 4, ['case', zoneMatchExpr, 1.6, 0.8], 10, ['case', zoneMatchExpr, 2.0, 1.4], 14, ['case', zoneMatchExpr, 2.8, 2.1], ] as unknown as never) : (['interpolate', ['linear'], ['zoom'], 4, 0.8, 10, 1.4, 14, 2.1] as never); if (map.getLayer(fillId)) { try { map.setPaintProperty( fillId, 'fill-opacity', hoveredZoneId ? (['case', zoneMatchExpr, 0.24, 0.1] as unknown as number) : 0.12, ); } catch { // ignore } } if (map.getLayer(lineId)) { try { map.setPaintProperty( lineId, 'line-color', hoveredZoneId ? (['case', zoneMatchExpr, 'rgba(125,211,252,0.98)', zoneColorExpr as never] as never) : (zoneColorExpr as never), ); } catch { // ignore } try { map.setPaintProperty(lineId, 'line-opacity', hoveredZoneId ? (['case', zoneMatchExpr, 1, 0.85] as never) : 0.85); } catch { // ignore } try { map.setPaintProperty(lineId, 'line-width', zoneLineWidthExpr); } catch { // ignore } } if (!map.getLayer(fillId)) { map.addLayer( { id: fillId, type: 'fill', source: srcId, paint: { 'fill-color': zoneColorExpr as never, 'fill-opacity': hoveredZoneId ? ([ 'case', zoneMatchExpr, 0.24, 0.1, ] as unknown as number) : 0.12, }, layout: { visibility }, } as unknown as LayerSpecification, before, ); } if (!map.getLayer(lineId)) { map.addLayer( { id: lineId, type: 'line', source: srcId, paint: { 'line-color': hoveredZoneId ? (['case', zoneMatchExpr, 'rgba(125,211,252,0.98)', zoneColorExpr as never] as never) : (zoneColorExpr as never), 'line-opacity': hoveredZoneId ? (['case', zoneMatchExpr, 1, 0.85] as never) : 0.85, 'line-width': zoneLineWidthExpr, }, layout: { visibility }, } as unknown as LayerSpecification, before, ); } if (!map.getLayer(labelId)) { map.addLayer( { id: labelId, type: 'symbol', source: srcId, layout: { visibility, 'symbol-placement': 'point', 'text-field': zoneLabelExpr as never, 'text-size': 11, 'text-font': ['Noto Sans Regular', 'Open Sans Regular'], 'text-anchor': 'top', 'text-offset': [0, 0.35], 'text-allow-overlap': false, 'text-ignore-placement': false, }, paint: { 'text-color': '#dbeafe', '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('Zones layer setup failed:', e); } finally { reorderGlobeFeatureLayers(); kickRepaint(map); } }; const stop = onMapStyleReady(map, ensure); return () => { stop(); }; }, [zones, overlays.zones, projection, baseMap, hoveredZoneId, mapSyncEpoch, reorderGlobeFeatureLayers]); }