/** * 지리 좌표 관련 유틸리티 함수 */ /** * Haversine 공식을 사용하여 두 좌표 간의 거리 계산 (km) * * @param lat1 - 첫 번째 지점의 위도 * @param lon1 - 첫 번째 지점의 경도 * @param lat2 - 두 번째 지점의 위도 * @param lon2 - 두 번째 지점의 경도 * @returns 두 지점 간의 거리 (km) */ export function calculateDistance( lat1: number, lon1: number, lat2: number, lon2: number ): number { const R = 6371; // 지구 반경 (km) const dLat = toRadians(lat2 - lat1); const dLon = toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } /** * 각도를 라디안으로 변환 */ function toRadians(degrees: number): number { return degrees * (Math.PI / 180); } /** * 라디안을 각도로 변환 */ export function toDegrees(radians: number): number { return radians * (180 / Math.PI); } /** * 좌표 배열에서 총 거리 계산 * * @param coordinates - { latitude, longitude }[] 형태의 좌표 배열 * @returns 총 거리 (km) */ export function calculateTotalDistance( coordinates: Array<{ latitude: number; longitude: number }> ): number { let totalDistance = 0; for (let i = 1; i < coordinates.length; i++) { const prev = coordinates[i - 1]; const curr = coordinates[i]; totalDistance += calculateDistance( prev.latitude, prev.longitude, curr.latitude, curr.longitude ); } return totalDistance; } /** * 좌표가 특정 반경 내에 있는지 확인 * * @param centerLat - 중심점 위도 * @param centerLon - 중심점 경도 * @param pointLat - 확인할 지점의 위도 * @param pointLon - 확인할 지점의 경도 * @param radiusKm - 반경 (km) * @returns 반경 내에 있으면 true */ export function isWithinRadius( centerLat: number, centerLon: number, pointLat: number, pointLon: number, radiusKm: number ): boolean { const distance = calculateDistance(centerLat, centerLon, pointLat, pointLon); return distance <= radiusKm; } /** * 두 좌표 사이의 방위각(bearing) 계산 * * @param lat1 - 시작점 위도 * @param lon1 - 시작점 경도 * @param lat2 - 도착점 위도 * @param lon2 - 도착점 경도 * @returns 방위각 (0-360도) */ export function calculateBearing( lat1: number, lon1: number, lat2: number, lon2: number ): number { const dLon = toRadians(lon2 - lon1); const lat1Rad = toRadians(lat1); const lat2Rad = toRadians(lat2); const x = Math.sin(dLon) * Math.cos(lat2Rad); const y = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLon); let bearing = toDegrees(Math.atan2(x, y)); bearing = (bearing + 360) % 360; // 0-360 범위로 정규화 return bearing; } /** * 좌표 배열의 경계 상자(bounding box) 계산 * * @param coordinates - 좌표 배열 * @returns { minLat, maxLat, minLon, maxLon } */ export function getBoundingBox( coordinates: Array<{ latitude: number; longitude: number }> ): { minLat: number; maxLat: number; minLon: number; maxLon: number } | null { if (coordinates.length === 0) return null; let minLat = coordinates[0].latitude; let maxLat = coordinates[0].latitude; let minLon = coordinates[0].longitude; let maxLon = coordinates[0].longitude; for (const coord of coordinates) { minLat = Math.min(minLat, coord.latitude); maxLat = Math.max(maxLat, coord.latitude); minLon = Math.min(minLon, coord.longitude); maxLon = Math.max(maxLon, coord.longitude); } return { minLat, maxLat, minLon, maxLon }; } /** * 좌표 배열의 중심점 계산 * * @param coordinates - 좌표 배열 * @returns { latitude, longitude } 중심점 */ export function getCenterPoint( coordinates: Array<{ latitude: number; longitude: number }> ): { latitude: number; longitude: number } | null { if (coordinates.length === 0) return null; let sumLat = 0; let sumLon = 0; for (const coord of coordinates) { sumLat += coord.latitude; sumLon += coord.longitude; } return { latitude: sumLat / coordinates.length, longitude: sumLon / coordinates.length, }; }