diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 4821c04..ca4fe06 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -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 ( <> @@ -981,11 +1065,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF boxShadow: `0 0 8px ${riskColor}`, }} /> - +