feat(korea): 임검침로 해상 루트 — 육지 우회 경유점 자동 삽입
- 한반도 해안 웨이포인트 14개 정의 (서해→남해→동해 시계방향) - 육지 바운딩박스 2개 (본토 + 제주도) - 직선이 육지 관통 시 해안 경유점 자동 삽입 - 시계/반시계 경로 중 짧은 쪽 자동 선택 - 직선 통과 가능 시 그대로 직선 유지 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
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,
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user