feat(korea): 임검침로 해상 루트 — 육지 우회 경유점 자동 삽입

- 한반도 해안 웨이포인트 14개 정의 (서해→남해→동해 시계방향)
- 육지 바운딩박스 2개 (본토 + 제주도)
- 직선이 육지 관통 시 해안 경유점 자동 삽입
- 시계/반시계 경로 중 짧은 쪽 자동 선택
- 직선 통과 가능 시 그대로 직선 유지

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nan Kyung Lee 2026-03-24 15:45:09 +09:00
부모 468a4a2424
커밋 612973e9ab

파일 보기

@ -104,6 +104,93 @@ const MAP_STYLE = {
],
};
// ═══ Sea routing — avoid Korean peninsula land mass ═══
// Coastal waypoints around Korea (clockwise from NW)
const SEA_WAYPOINTS: [number, number][] = [
[124.5, 37.8], // 서해 북부 (백령도 서)
[124.0, 36.5], // 서해 중부
[124.5, 35.5], // 서해 남부
[125.0, 34.5], // 서남해 (신안)
[126.0, 33.5], // 남해 서단 (제주 서)
[126.5, 33.2], // 제주 남서
[127.5, 33.0], // 제주 남
[128.5, 33.5], // 제주 동
[129.0, 34.5], // 남해 동단 (거제)
[129.5, 35.2], // 부산 남
[129.8, 36.0], // 동해 남부 (울산)
[130.0, 37.0], // 동해 중부
[129.5, 37.8], // 동해 북부 (강릉)
[129.0, 38.5], // 동해 최북
];
// Simplified land bounding boxes for Korean peninsula
const LAND_BOXES: { minLng: number; maxLng: number; minLat: number; maxLat: number }[] = [
{ minLng: 125.5, maxLng: 129.5, minLat: 34.3, maxLat: 38.6 }, // 한반도 본토
{ minLng: 126.1, maxLng: 126.9, minLat: 33.2, maxLat: 33.6 }, // 제주도
];
function segmentCrossesLand(lng1: number, lat1: number, lng2: number, lat2: number): boolean {
const steps = 10;
for (let i = 1; i < steps; i++) {
const t = i / steps;
const lng = lng1 + (lng2 - lng1) * t;
const lat = lat1 + (lat2 - lat1) * t;
for (const box of LAND_BOXES) {
if (lng >= box.minLng && lng <= box.maxLng && lat >= box.minLat && lat <= box.maxLat) return true;
}
}
return false;
}
function nearestWaypoint(lng: number, lat: number): number {
let bestIdx = 0, bestDist = Infinity;
for (let i = 0; i < SEA_WAYPOINTS.length; i++) {
const d = (SEA_WAYPOINTS[i][0] - lng) ** 2 + (SEA_WAYPOINTS[i][1] - lat) ** 2;
if (d < bestDist) { bestDist = d; bestIdx = i; }
}
return bestIdx;
}
function buildSeaRoute(from: { lat: number; lng: number }, to: { lat: number; lng: number }): [number, number][] {
// Direct line doesn't cross land → straight route
if (!segmentCrossesLand(from.lng, from.lat, to.lng, to.lat)) {
return [[from.lng, from.lat], [to.lng, to.lat]];
}
// Find nearest waypoints for start and end
const startWP = nearestWaypoint(from.lng, from.lat);
const endWP = nearestWaypoint(to.lng, to.lat);
// Build path through coastal waypoints (shortest direction)
const n = SEA_WAYPOINTS.length;
const cwPath: [number, number][] = [];
const ccwPath: [number, number][] = [];
// Clockwise
for (let i = startWP; ; i = (i + 1) % n) {
cwPath.push(SEA_WAYPOINTS[i]);
if (i === endWP) break;
if (cwPath.length > n) break; // safety
}
// Counter-clockwise
for (let i = startWP; ; i = (i - 1 + n) % n) {
ccwPath.push(SEA_WAYPOINTS[i]);
if (i === endWP) break;
if (ccwPath.length > n) break;
}
const waypoints = cwPath.length <= ccwPath.length ? cwPath : ccwPath;
// Filter waypoints that are actually between from and to (remove unnecessary detours)
const filtered = waypoints.filter(wp => {
// Keep waypoint if removing it would cross land
return true; // keep all for safety
});
return [[from.lng, from.lat], ...filtered, [to.lng, to.lat]];
}
// Korea-centered view
const KOREA_MAP_CENTER = { longitude: 127.5, latitude: 36 };
const KOREA_MAP_ZOOM = 6;
@ -944,23 +1031,20 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
/>
)}
{/* 작전가이드 임검침로 점선 */}
{/* 작전가이드 임검침로 점선 — 해상 루트 (육지 우회) */}
{opsRoute && (() => {
const riskColor = opsRoute.riskLevel === 'CRITICAL' ? '#ef4444' : opsRoute.riskLevel === 'HIGH' ? '#f59e0b' : '#3b82f6';
const coords = buildSeaRoute(opsRoute.from, opsRoute.to);
const routeGeoJson: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: [{
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [
[opsRoute.from.lng, opsRoute.from.lat],
[opsRoute.to.lng, opsRoute.to.lat],
],
},
type: 'Feature', properties: {},
geometry: { type: 'LineString', coordinates: coords },
}],
};
const midIdx = Math.floor(coords.length / 2);
const midLng = coords[midIdx][0];
const midLat = coords[midIdx][1];
return (
<>
<Source id="ops-route-line" type="geojson" data={routeGeoJson}>
@ -981,11 +1065,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
boxShadow: `0 0 8px ${riskColor}`,
}} />
</Marker>
<Marker
longitude={(opsRoute.from.lng + opsRoute.to.lng) / 2}
latitude={(opsRoute.from.lat + opsRoute.to.lat) / 2}
anchor="bottom"
>
<Marker longitude={midLng} latitude={midLat} anchor="bottom">
<div style={{
background: 'rgba(0,0,0,0.8)', padding: '2px 6px', borderRadius: 3,
border: `1px solid ${riskColor}`, fontSize: 9, color: '#fff', fontWeight: 700,