지도 엔진을 Leaflet 1.9에서 MapLibre GL JS 5.x + deck.gl 9.x로 전환. 15개 파일 수정, Leaflet 완전 제거. WebGL 단일 canvas로 z-index 충돌 해결, 유류 입자 ScatterplotLayer GPU 렌더링으로 10~100배 성능 향상. - MapView.tsx: MapLibre Map + DeckGLOverlay(MapboxOverlay interleaved) - 유류 입자/오일펜스/HNS: deck.gl ScatterplotLayer/PathLayer - 역추적 리플레이: createBacktrackLayers() 함수 패턴 - 기상 오버레이: WeatherMapOverlay/OceanCurrent/WindParticle deck.gl 전환 - 수온 히트맵: WaterTemperatureLayer deck.gl ScatterplotLayer - 해황예보도: MapLibre image source + raster layer - SCAT/Assets/Incidents: MapLibre Map + deck.gl 레이어 - WMS 밝기: raster-brightness-min/max 네이티브 속성 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
149 lines
4.5 KiB
TypeScript
Executable File
149 lines
4.5 KiB
TypeScript
Executable File
import { useMemo } from 'react'
|
|
import { ScatterplotLayer } from '@deck.gl/layers'
|
|
import type { Layer } from '@deck.gl/core'
|
|
import { hexToRgba } from '@common/components/map/mapUtils'
|
|
|
|
interface OceanCurrentData {
|
|
lat: number
|
|
lon: number
|
|
direction: number // 0-360도
|
|
speed: number // m/s
|
|
}
|
|
|
|
interface OceanCurrentLayerProps {
|
|
visible: boolean
|
|
opacity?: number
|
|
}
|
|
|
|
// 한반도 육지 영역 판별 (간략화된 폴리곤)
|
|
const isOnLand = (lat: number, lon: number): boolean => {
|
|
const peninsula: [number, number][] = [
|
|
[38.5, 124.5], [38.5, 128.3],
|
|
[37.8, 128.8], [37.0, 129.2],
|
|
[36.0, 129.5], [35.1, 129.2],
|
|
[34.8, 128.6], [34.5, 127.8],
|
|
[34.3, 126.5], [34.8, 126.1],
|
|
[35.5, 126.0], [36.0, 126.3],
|
|
[36.8, 126.0], [37.5, 126.2],
|
|
[38.5, 124.5],
|
|
]
|
|
|
|
// 제주도 영역
|
|
if (lat >= 33.1 && lat <= 33.7 && lon >= 126.1 && lon <= 127.0) return true
|
|
|
|
// Ray casting algorithm
|
|
let inside = false
|
|
for (let i = 0, j = peninsula.length - 1; i < peninsula.length; j = i++) {
|
|
const [yi, xi] = peninsula[i]
|
|
const [yj, xj] = peninsula[j]
|
|
if ((yi > lat) !== (yj > lat) && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi) {
|
|
inside = !inside
|
|
}
|
|
}
|
|
return inside
|
|
}
|
|
|
|
// 한국 해역의 대략적인 해류 데이터 생성
|
|
const generateOceanCurrentData = (): OceanCurrentData[] => {
|
|
const data: OceanCurrentData[] = []
|
|
|
|
for (let lat = 33.5; lat <= 38.0; lat += 0.8) {
|
|
for (let lon = 125.0; lon <= 130.5; lon += 0.8) {
|
|
if (isOnLand(lat, lon)) continue
|
|
|
|
let direction = 0
|
|
let speed = 0.3
|
|
|
|
if (lon > 128.5) {
|
|
// 동해 — 북동진하는 동한난류
|
|
direction = 30 + Math.random() * 20
|
|
speed = 0.4 + Math.random() * 0.3
|
|
} else if (lon < 126.5) {
|
|
// 서해 — 북진
|
|
direction = 350 + Math.random() * 20
|
|
speed = 0.2 + Math.random() * 0.2
|
|
} else {
|
|
// 남해 — 동진
|
|
direction = 80 + Math.random() * 20
|
|
speed = 0.3 + Math.random() * 0.3
|
|
}
|
|
|
|
data.push({ lat, lon, direction, speed })
|
|
}
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
// 속도에 따른 hex 색상
|
|
function getCurrentHexColor(speed: number): string {
|
|
if (speed > 0.5) return '#ef4444'
|
|
if (speed > 0.3) return '#f59e0b'
|
|
return '#3b82f6'
|
|
}
|
|
|
|
// 해류 데이터는 컴포넌트 외부에서 한 번만 생성 (랜덤이므로 안정화)
|
|
const OCEAN_CURRENT_DATA = generateOceanCurrentData()
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
/**
|
|
* OceanCurrentLayer — deck.gl ScatterplotLayer 배열 반환 훅
|
|
*
|
|
* 기존: Leaflet Marker + SVG DivIcon + CSS rotate
|
|
* 전환: ScatterplotLayer (크기=속도, 색상=방향)
|
|
*
|
|
* 방향 표현 한계: deck.gl ScatterplotLayer는 원형이므로 방향을 색상으로 구분
|
|
* - 북동(0~90°): 파랑
|
|
* - 남동(90~180°): 초록
|
|
* - 남서(180~270°): 주황
|
|
* - 북서(270~360°): 빨강
|
|
* 속도는 반경 크기(5~20km)로 표현
|
|
*/
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export function useOceanCurrentLayers(props: OceanCurrentLayerProps): Layer[] {
|
|
const { visible, opacity = 0.7 } = props
|
|
|
|
return useMemo(() => {
|
|
if (!visible) return []
|
|
|
|
const data = OCEAN_CURRENT_DATA.map((c) => ({
|
|
position: [c.lon, c.lat] as [number, number],
|
|
// 반경: 속도 비례 (5~20km)
|
|
radius: 5000 + c.speed * 25000,
|
|
fillColor: hexToRgba(getCurrentHexColor(c.speed), Math.round(opacity * 180)),
|
|
lineColor: hexToRgba(getCurrentHexColor(c.speed), Math.round(opacity * 230)),
|
|
}))
|
|
|
|
return [
|
|
new ScatterplotLayer({
|
|
id: 'ocean-current-layer',
|
|
data,
|
|
getPosition: (d) => d.position,
|
|
getRadius: (d) => d.radius,
|
|
getFillColor: (d) => d.fillColor,
|
|
getLineColor: (d) => d.lineColor,
|
|
getLineWidth: 1,
|
|
stroked: true,
|
|
radiusUnits: 'meters',
|
|
pickable: false,
|
|
updateTriggers: {
|
|
getFillColor: [opacity],
|
|
getLineColor: [opacity],
|
|
},
|
|
}) as unknown as Layer,
|
|
]
|
|
}, [visible, opacity])
|
|
}
|
|
|
|
/**
|
|
* OceanCurrentLayer — React 컴포넌트 (null 반환, layers는 useOceanCurrentLayers로 분리)
|
|
*
|
|
* WeatherView에서 deck.gl layers 배열에 useOceanCurrentLayers() 결과를 주입하여 사용한다.
|
|
* 이 컴포넌트는 이전 Leaflet 방식과의 호환성을 위해 유지하되 실제 렌더링은 하지 않는다.
|
|
*/
|
|
export function OceanCurrentLayer(props: OceanCurrentLayerProps) {
|
|
// visible 상태 변화에 따른 side-effect 없음 — 렌더링은 useOceanCurrentLayers 훅이 담당
|
|
void props
|
|
return null
|
|
}
|