231 lines
7.4 KiB
TypeScript
231 lines
7.4 KiB
TypeScript
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<maplibregl.Map | null>,
|
|
projectionBusyRef: MutableRefObject<boolean>,
|
|
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]);
|
|
}
|